use std::time::SystemTime; use crate::{DomainError, NoteId, ProfileId, SpaceId, TabId, UrlText}; #[derive(Clone, Debug, Eq, PartialEq)] pub enum NoteTarget { Url(UrlText), Tab(TabId), } #[derive(Clone, Debug, Eq, PartialEq)] pub struct NoteEntry { id: NoteId, profile_id: ProfileId, space_id: SpaceId, target: NoteTarget, title: String, source_url: UrlText, body: String, created_at: SystemTime, updated_at: SystemTime, } impl NoteEntry { pub fn new( profile_id: ProfileId, space_id: SpaceId, target: NoteTarget, title: impl Into, source_url: UrlText, body: impl Into, now: SystemTime, ) -> Result { let title = non_empty_text("note title", title.into())?; let body = normalize_body(body.into())?; Ok(Self { id: NoteId::new(), profile_id, space_id, target, title, source_url, body, created_at: now, updated_at: now, }) } #[must_use] pub fn id(&self) -> &NoteId { &self.id } #[must_use] pub fn profile_id(&self) -> &ProfileId { &self.profile_id } #[must_use] pub fn space_id(&self) -> &SpaceId { &self.space_id } #[must_use] pub fn target(&self) -> &NoteTarget { &self.target } #[must_use] pub fn target_label(&self) -> &'static str { match &self.target { NoteTarget::Url(_) => "URL note", NoteTarget::Tab(_) => "Tab note", } } #[must_use] pub fn title(&self) -> &str { &self.title } #[must_use] pub fn source_url(&self) -> &UrlText { &self.source_url } #[must_use] pub fn display_url(&self) -> String { self.source_url.display_url() } #[must_use] pub fn body(&self) -> &str { &self.body } #[must_use] pub fn created_at(&self) -> SystemTime { self.created_at } #[must_use] pub fn updated_at(&self) -> SystemTime { self.updated_at } pub fn update( &mut self, title: impl Into, source_url: UrlText, body: impl Into, updated_at: SystemTime, ) -> Result<(), DomainError> { self.title = non_empty_text("note title", title.into())?; self.source_url = source_url; self.body = normalize_body(body.into())?; self.updated_at = updated_at; Ok(()) } } fn normalize_body(value: String) -> Result { let normalized = value.replace("\r\n", "\n").replace('\r', "\n"); non_empty_text("note body", normalized) } fn non_empty_text(field: &'static str, value: String) -> Result { let trimmed = value.trim(); if trimmed.is_empty() { return Err(DomainError::EmptyField { field }); } Ok(trimmed.to_string()) } #[cfg(test)] mod tests { use std::time::{Duration, SystemTime}; use super::{NoteEntry, NoteTarget}; use crate::{DomainError, ProfileId, SpaceId, TabId, UrlText}; #[test] fn creates_url_note_with_markdown_body() -> Result<(), DomainError> { let now = SystemTime::UNIX_EPOCH + Duration::from_secs(10); let source_url = UrlText::parse("https://example.com/article")?; let note = NoteEntry::new( ProfileId::new(), SpaceId::new(), NoteTarget::Url(source_url.clone()), "Example", source_url, " # Heading\r\n- item ", now, )?; assert_eq!(note.title(), "Example"); assert_eq!(note.body(), "# Heading\n- item"); assert_eq!(note.target_label(), "URL note"); assert_eq!(note.created_at(), now); assert_eq!(note.updated_at(), now); Ok(()) } #[test] fn creates_tab_note_target() -> Result<(), DomainError> { let tab_id = TabId::new(); let note = NoteEntry::new( ProfileId::new(), SpaceId::new(), NoteTarget::Tab(tab_id.clone()), "Example", UrlText::parse("https://example.com/tab")?, "- detail", SystemTime::UNIX_EPOCH, )?; assert_eq!(note.target(), &NoteTarget::Tab(tab_id)); assert_eq!(note.target_label(), "Tab note"); Ok(()) } #[test] fn rejects_empty_body() -> Result<(), DomainError> { let source_url = UrlText::parse("https://example.com")?; let result = NoteEntry::new( ProfileId::new(), SpaceId::new(), NoteTarget::Url(source_url.clone()), "Example", source_url, " ", SystemTime::UNIX_EPOCH, ); assert_eq!(result, Err(DomainError::EmptyField { field: "note body" })); Ok(()) } #[test] fn updates_body_and_timestamp() -> Result<(), DomainError> { let created_at = SystemTime::UNIX_EPOCH + Duration::from_secs(10); let updated_at = created_at + Duration::from_secs(30); let mut note = NoteEntry::new( ProfileId::new(), SpaceId::new(), NoteTarget::Url(UrlText::parse("https://example.com/old")?), "Old", UrlText::parse("https://example.com/old")?, "first", created_at, )?; note.update( "New", UrlText::parse("https://example.com/new")?, " second\rline ", updated_at, )?; assert_eq!(note.title(), "New"); assert_eq!(note.source_url().as_str(), "https://example.com/new"); assert_eq!(note.body(), "second\nline"); assert_eq!(note.created_at(), created_at); assert_eq!(note.updated_at(), updated_at); Ok(()) } }