From 4e78fd83349c95711cdee5acc56f248f81ebd25c Mon Sep 17 00:00:00 2001 From: Mikkel Thestrup Date: Mon, 26 Jan 2026 21:27:59 +0100 Subject: feat(domain): add core domain models for calendar application Implement domain-driven design layer with entities, value objects, and repository traits for calendar management system. Domain Entities: - Calendar: manages calendar lifecycle and archival state - Event: handles single occurrence events with cancellation support - RecurringEvent: supports recurring events with exception handling - RecurrenceException: manages modifications to specific occurrences Value Objects: - TimeRange: enforces valid start/end time constraints - EventColor: type-safe color representation - Frequency: recurrence frequency enumeration (daily/weekly/monthly/yearly) - RecurrenceRule: encapsulates recurrence pattern logic - CalendarId & EventId: essentially just a wrapper Repository Traits: - CalendarRepository: calendar persistence interface - EventRepository: event querying and persistence with overlap detection - RecurrenceRepository: recurring event management Key Design Decisions: - Use Uuid for entity IDs (type safety, performance) - Encapsulate business logic within entities - Immutable value objects with validation - Repository pattern for infrastructure abstraction - Clear separation of concerns between domain and persistence layers All entities include timestamp tracking and follow builder pattern for construction with validation at domain boundaries. --- src/domain/value_objects.rs | 147 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100755 src/domain/value_objects.rs (limited to 'src/domain/value_objects.rs') diff --git a/src/domain/value_objects.rs b/src/domain/value_objects.rs new file mode 100755 index 0000000..f38d351 --- /dev/null +++ b/src/domain/value_objects.rs @@ -0,0 +1,147 @@ +use core::fmt; + +use chrono::{DateTime, Utc}; +use getset::{Getters}; +use uuid::Uuid; + +use crate::domain::error::DomainError; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EventColor(u8); + +impl From for EventColor { + fn from(value: u8) -> Self { + Self(value) + } +} + +impl From for u8 { + fn from(color: EventColor) -> Self { + color.0 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Getters)] +pub struct TimeRange { + #[getset(get = "pub")] + starts_at: DateTime, + #[getset(get = "pub")] + ends_at: DateTime, +} + +impl TimeRange { + pub fn new( + starts_at: DateTime, + ends_at: DateTime, + ) -> Result { + if starts_at >= ends_at { + Err(DomainError::InvalidTimeRange) + } else { + Ok(Self { starts_at, ends_at }) + } + } + + pub fn overlaps(&self, other: &Self) -> bool { + self.starts_at < other.ends_at + && other.starts_at < self.ends_at + } + + pub fn duration(&self) -> chrono::Duration { + self.ends_at - self.starts_at + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Frequency { + Daily, + Weekly, + Monthly, + Yearly, +} + +impl fmt::Display for Frequency { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Frequency::Daily => write!(f, "DAILY"), + Frequency::Weekly => write!(f, "WEEKLY"), + Frequency::Monthly => write!(f, "MONTHLY"), + Frequency::Yearly => write!(f, "YEARLY"), + } + } +} + +impl std::str::FromStr for Frequency { + type Err = DomainError; + + fn from_str(s: &str) -> Result { + match s.to_uppercase().as_str() { + "DAILY" => Ok(Frequency::Daily), + "WEEKLY" => Ok(Frequency::Weekly), + "MONTHLY" => Ok(Frequency::Monthly), + "YEARLY" => Ok(Frequency::Yearly), + _ => Err(DomainError::InvalidFrequency), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct CalendarId(Uuid); + +impl CalendarId { + pub fn new() -> Self { + Self(Uuid::new_v4()) + } + + pub fn from_uuid(uuid: Uuid) -> Self { + Self(uuid) + } + + pub fn as_uuid(&self) -> Uuid { + self.0 + } +} + +impl std::fmt::Display for CalendarId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::str::FromStr for CalendarId { + type Err = uuid::Error; + + fn from_str(s: &str) -> Result { + Ok(Self(Uuid::parse_str(s)?)) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct EventId(Uuid); + +impl EventId { + pub fn new() -> Self { + Self(Uuid::new_v4()) + } + + pub fn from_uuid(uuid: Uuid) -> Self { + Self(uuid) + } + + pub fn as_uuid(&self) -> Uuid { + self.0 + } +} + +impl std::fmt::Display for EventId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::str::FromStr for EventId { + type Err = uuid::Error; + + fn from_str(s: &str) -> Result { + Ok(Self(Uuid::parse_str(s)?)) + } +} -- cgit v1.2.3-70-g09d2