diff options
| author | Mikkel Thestrup <mikkel@mithe.dk> | 2026-01-26 21:27:59 +0100 |
|---|---|---|
| committer | Mikkel Thestrup <mikkel@mithe.dk> | 2026-01-27 15:37:03 +0100 |
| commit | 4e78fd83349c95711cdee5acc56f248f81ebd25c (patch) | |
| tree | 6a380da4c3651f73a806cafc9222012af9564f56 /src/domain/recurrence.rs | |
| parent | 4b1074193991a510fd2129513d5fcb7c6da933d2 (diff) | |
| download | kal-4e78fd83349c95711cdee5acc56f248f81ebd25c.tar.gz kal-4e78fd83349c95711cdee5acc56f248f81ebd25c.zip | |
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.
Diffstat (limited to '')
| -rw-r--r-- | src/domain/recurrence.rs | 200 |
1 files changed, 200 insertions, 0 deletions
diff --git a/src/domain/recurrence.rs b/src/domain/recurrence.rs new file mode 100644 index 0000000..263868e --- /dev/null +++ b/src/domain/recurrence.rs @@ -0,0 +1,200 @@ +use std::collections::HashMap; + +use chrono::{DateTime, Utc}; +use getset::Getters; + +use crate::domain::{ + error::DomainError, + value_objects::{CalendarId, EventColor, EventId, Frequency, TimeRange} +}; + +#[derive(Debug, Clone, Getters)] +pub struct RecurrenceRule { + #[getset(get = "pub")] + frequency: Frequency, + #[getset(get = "pub")] + interval: u32, + #[getset(get = "pub")] + until: Option<DateTime<Utc>>, +} + +impl RecurrenceRule { + pub fn new( + frequency: Frequency, + interval: u32, + until: Option<DateTime<Utc>>, + ) -> Result<Self, DomainError> { + if interval == 0 { + return Err(DomainError::InvalidInterval); + } + Ok(Self { + frequency, + interval, + until, + }) + } +} + +#[derive(Debug, Clone, Getters)] +pub struct RecurringEvent { + #[getset(get = "pub")] + id: EventId, + #[getset(get = "pub")] + calendar_id: CalendarId, + #[getset(get = "pub")] + title: String, + #[getset(get = "pub")] + description: Option<String>, + #[getset(get = "pub")] + time_range: TimeRange, + #[getset(get = "pub")] + rule: RecurrenceRule, + #[getset(get = "pub")] + exceptions: HashMap<DateTime<Utc>, RecurrenceException>, + #[getset(get = "pub")] + color: EventColor, + #[getset(get = "pub")] + is_all_day: bool, + #[getset(get = "pub")] + is_cancelled: bool, + #[getset(get = "pub")] + created_at: DateTime<Utc>, + #[getset(get = "pub")] + updated_at: DateTime<Utc>, +} + +impl RecurringEvent { + pub fn new( + calendar_id: CalendarId, + title: String, + description: Option<String>, + time_range: TimeRange, + rule: RecurrenceRule, + color: EventColor, + is_all_day: bool, + ) -> Self { + let now = Utc::now(); + Self { + id: EventId::new(), + calendar_id, + title, + description, + time_range, + rule, + exceptions: HashMap::new(), + color, + is_all_day, + is_cancelled: false, + created_at: now, + updated_at: now, + } + } + + pub fn with_id( + id: EventId, + calendar_id: CalendarId, + title: String, + description: Option<String>, + time_range: TimeRange, + rule: RecurrenceRule, + exceptions: HashMap<DateTime<Utc>, RecurrenceException>, + color: EventColor, + is_all_day: bool, + is_cancelled: bool, + created_at: DateTime<Utc>, + updated_at: DateTime<Utc>, + ) -> Self { + Self { + id, + calendar_id, + title, + description, + time_range, + rule, + exceptions, + color, + is_all_day, + is_cancelled, + created_at, + updated_at, + } + } + + pub fn add_exception(&mut self, exception: RecurrenceException) { + self.exceptions.insert(exception.original_starts_at, exception); + self.touch(); + } + + pub fn remove_exception(&mut self, original_starts_at: DateTime<Utc>) { + self.exceptions.remove(&original_starts_at); + self.touch(); + } + + pub fn cancel_occurrence(&mut self, original_starts_at: DateTime<Utc>) { + let exception = RecurrenceException::cancelled(original_starts_at); + self.add_exception(exception); + } + + pub fn reschedule_occurrence( + &mut self, + original_starts_at: DateTime<Utc>, + new_time_range: TimeRange, + ) { + let exception = RecurrenceException::rescheduled( + original_starts_at, + new_time_range + ); + self.add_exception(exception); + } + + pub fn cancel(&mut self) { + self.is_cancelled = true; + self.touch(); + } + + pub fn touch(&mut self) { + self.updated_at = Utc::now(); + } +} + +#[derive(Debug, Clone, Getters)] +pub struct RecurrenceException { + #[getset(get = "pub")] + original_starts_at: DateTime<Utc>, + #[getset(get = "pub")] + modification: ExceptionModification, +} + +#[derive(Debug, Clone)] +pub enum ExceptionModification { + Cancelled, + Rescheduled { new_time_range: TimeRange }, +} + +impl RecurrenceException { + pub fn cancelled(original_starts_at: DateTime<Utc>) -> Self { + Self { + original_starts_at, + modification: ExceptionModification::Cancelled, + } + } + + pub fn rescheduled( + original_starts_at: DateTime<Utc>, + new_time_range: TimeRange, + ) -> Self { + Self { + original_starts_at, + modification: ExceptionModification::Rescheduled { new_time_range }, + } + } + + pub fn new_time_range(&self) -> Option<&TimeRange> { + match &self.modification { + ExceptionModification::Rescheduled { + new_time_range, + } => Some(new_time_range), + ExceptionModification::Cancelled => None, + } + } +} |