diff options
| author | Mikkel Thestrup <mikkel_thestrup@mithe.dk> | 2025-12-09 14:52:58 +0100 |
|---|---|---|
| committer | Mikkel Thestrup <mikkel_thestrup@mithe.dk> | 2025-12-09 17:17:56 +0100 |
| commit | 147125358b66c2bf097ed11f82042e220a730090 (patch) | |
| tree | 2b6957d01a1afb0553996e916faea5c646f5cc6e | |
| parent | 4b1074193991a510fd2129513d5fcb7c6da933d2 (diff) | |
| download | kal-master.tar.gz kal-master.zip | |
- Added `calendar.rs` with Calendar entity and builder
- Added `event.rs` with Event model and builder
- Added `recurrence.rs` for recurrence rules
- Added `mod.rs` to expose domain module structure
These files establish the core domain layer structures
for future business logic.
| -rw-r--r-- | src/domain/calendar.rs | 108 | ||||
| -rw-r--r-- | src/domain/event.rs | 310 | ||||
| -rw-r--r-- | src/domain/mod.rs | 6 | ||||
| -rw-r--r-- | src/domain/recurrence.rs | 36 |
4 files changed, 460 insertions, 0 deletions
diff --git a/src/domain/calendar.rs b/src/domain/calendar.rs new file mode 100644 index 0000000..9e6a7fe --- /dev/null +++ b/src/domain/calendar.rs @@ -0,0 +1,108 @@ +use std::collections::BTreeMap; + +use chrono::{DateTime, NaiveDate, Utc}; +use getset::Getters; +use uuid::Uuid; + +use crate::domain::Event; + +#[derive(Clone, Debug, Getters)] +#[getset(get = "pub")] +pub struct Calendar { + id: Uuid, + name: String, + description: Option<String>, + events: BTreeMap<NaiveDate, Vec<Event>>, + is_archived: bool, + created_at: DateTime<Utc>, + updated_at: DateTime<Utc>, +} + +pub struct CalendarBuilder { + name: String, + description: Option<String>, + events: BTreeMap<NaiveDate, Vec<Event>>, + is_archived: bool, +} + +impl CalendarBuilder { + pub fn new(name: String) -> Result<Self, String> { + if name.is_empty() { + Err("Name cannot be empty".to_string()) + } else { + Ok(Self { + name, + description: None, + events: BTreeMap::new(), + is_archived: false + }) + } + } + + pub fn description(mut self, description: Option<String>) -> Self { + self.description = description; + self + } + + pub fn events(mut self, events: BTreeMap<NaiveDate, Vec<Event>>) -> Self { + self.events = events; + self + } + + pub fn is_archived(mut self, is_archived: bool) -> Self { + self.is_archived = is_archived; + self + } + + pub fn build(self) -> Calendar { + let now = Utc::now(); + Calendar { + id: Uuid::new_v4(), + name: self.name, + description: self.description, + events: self.events, + is_archived: self.is_archived, + created_at: now, + updated_at: now + } + } +} + +impl Calendar { + pub fn add_events(&mut self, events: Vec<Event>) { + for event in events { + let date = event.start().date_naive(); + + self.events + .entry(date) + .or_insert_with(Vec::new) + .push(event); + } + + self.touch() + } + + pub fn update_name(&mut self, name: String) -> Result<(), String> { + if name.is_empty() { + Err("Name cannot be empty".to_string()) + } else { + self.name = name; + self.touch(); + Ok(()) + } + } + + pub fn update_description(&mut self, description: Option<String>) { + self.description = description; + self.touch() + } + + pub fn update_archived(&mut self, is_archived: bool) { + self.is_archived = is_archived; + self.touch() + } + + pub fn touch(&mut self) { + self.updated_at = Utc::now() + } +} diff --git a/src/domain/event.rs b/src/domain/event.rs new file mode 100644 index 0000000..6ad24a4 --- /dev/null +++ b/src/domain/event.rs @@ -0,0 +1,310 @@ +//! Calendar event domain model. +//! +//! This module provides the core `Event` and `EventBuilder` types for representing +//! calendar events with recurrence support, metadata tracking, and validation. +//! +//! # Overview +//! +//! The `Event` struct represents a single calendar entry +//! with start and end times, optional recurrence rules, and metadata. +//! Use the `EventBuilder` to construct events with validation of required fields +//! and sensible defaults for optional ones. +//! +//! # Examples +//! +//! ``` +//! # use uuid::Uuid; +//! # use chrono::Utc; +//! # let cal_id = Uuid::new_v4(); +//! let event = EventBuilder::new( +//! cal_id, +//! "Team Meeting".to_string(), +//! Utc::now(), +//! Utc::now() + chrono::Duration::hours(1) +//! )? +//! .color(5) +//! .description(Some("Quarterly sync".to_string())) +//! .build(); +//! # Ok::<(), String>(()) +//! ``` + +use chrono::{DateTime, TimeDelta, Utc}; +use getset::Getters; +use uuid::Uuid; +use crate::domain::recurrence::RecurrenceRule; + +/// A calendar event with optional recurrence, metadata, and state tracking. +/// +/// `Event` represents a single calendar entry with start and end times, +/// optional recurrence rules, and metadata like color and cancellation status. +/// All mutation operations update the `updated_at` timestamp. +/// +/// # Fields +/// +/// * `id` - Unique identifier for the event +/// * `calendar_id` - The calendar this event belongs to +/// * `title` - Display title of the event +/// * `description` - Optional detailed description +/// * `start` - Event start time in UTC +/// * `end` - Event end time in UTC +/// * `recurring` - Optional recurrence rule for repeating events +/// * `created_at` - When the event was created +/// * `updated_at` - When the event was last modified +/// * `color` - Color identifier (0-255) +/// * `is_all_day` - Whether this is an all-day event +/// * `is_cancelled` - Whether this event is cancelled +#[derive(Clone, Debug, Getters)] +#[getset(get = "pub")] +pub struct Event { + id: Uuid, + calendar_id: Uuid, + title: String, + description: Option<String>, + start: DateTime<Utc>, + end: DateTime<Utc>, + recurring: Option<RecurrenceRule>, + created_at: DateTime<Utc>, + updated_at: DateTime<Utc>, + color: u8, + is_all_day: bool, + is_cancelled: bool +} + +/// Builder for constructing `Event` instances with validation. +/// +/// Provides an ergonomic way to create events with required field validation. +/// Required fields are `calendar_id`, `title`, `start`, and `end`. +/// Optional fields have sensible defaults. +/// +/// # Examples +/// +/// ``` +/// # use uuid::Uuid; +/// # use chrono::Utc; +/// # let cal_id = Uuid::new_v4(); +/// let event = EventBuilder::new( +/// cal_id, +/// "Team Meeting".to_string(), +/// Utc::now(), +/// Utc::now() + chrono::Duration::hours(1) +/// )? +/// .color(5) +/// .description(Some("Quarterly sync".to_string())) +/// .build(); +/// ``` +pub struct EventBuilder { + calendar_id: Uuid, + title: String, + start: DateTime<Utc>, + end: DateTime<Utc>, + description: Option<String>, + recurring: Option<RecurrenceRule>, + color: u8, + is_all_day: bool, + is_cancelled: bool, +} + +impl EventBuilder { + /// Creates a new `EventBuilder` with required fields. + /// + /// # Arguments + /// + /// * `calendar_id` - The calendar to which this event belongs + /// * `title` - The display title of the event (cannot be empty) + /// * `start` - The event start time in UTC + /// * `end` - The event end time in UTC (must be after `start`) + /// + /// # Errors + /// + /// Returns an error if: + /// - `title` is empty + /// - `start` is not strictly before `end` + pub fn new( + calendar_id: Uuid, + title: String, + start: DateTime<Utc>, + end: DateTime<Utc> + ) -> Result<Self, String> { + if title.is_empty() { + Err("Title cannot be empty".to_string()) + } else if start >= end { + Err("Start time must be strictly before end time".to_string()) + } else { + Ok(Self { + calendar_id, + title, + start, + end, + description: None, + recurring: None, + color: 0, + is_all_day: false, + is_cancelled: false + }) + } + } + + /// Sets the event description. + /// + /// # Arguments + /// + /// * `description` - Optional detailed description of the event + pub fn description(mut self, description: Option<String>) -> Self { + self.description = description; + self + } + + /// Sets the recurrence rule for repeating events. + /// + /// # Arguments + /// + /// * `recurring` - Optional recurrence rule defining repetition pattern + pub fn recurring(mut self, recurring: Option<RecurrenceRule>) -> Self { + self.recurring = recurring; + self + } + + /// Sets the color identifier for the event. + /// + /// # Arguments + /// + /// * `color` - Color identifier in range 0-255 + pub fn color(mut self, color: u8) -> Self { + self.color = color; + self + } + + /// Sets whether this is an all-day event. + /// + /// # Arguments + /// + /// * `is_all_day` - `true` if this is an all-day event, `false` otherwise + pub fn is_all_day(mut self, is_all_day: bool) -> Self { + self.is_all_day = is_all_day; + self + } + + /// Sets whether this event is cancelled. + /// + /// # Arguments + /// + /// * `is_cancelled` - `true` if this event is cancelled, `false` otherwise + pub fn is_cancelled(mut self, is_cancelled: bool) -> Self { + self.is_cancelled = is_cancelled; + self + } + + /// Builds and returns the constructed `Event`. + /// + /// Sets the `id`, `created_at`, and `updated_at` fields to automatically + /// generated values. The `id` is a new UUID, and both timestamps are set + /// to the current UTC time. + pub fn build(self) -> Event { + let now = Utc::now(); + Event { + id: Uuid::new_v4(), + calendar_id: self.calendar_id, + title: self.title, + description: self.description, + start: self.start, + end: self.end, + recurring: self.recurring, + created_at: now, + updated_at: now, + color: self.color, + is_all_day: self.is_all_day, + is_cancelled: self.is_cancelled, + } + } +} + +impl Event { + /// Updates the event title. + /// + /// # Errors + /// Returns an error if the title is empty. + pub fn update_title(&mut self, title: String) -> Result<(), String> { + if title.is_empty() { + Err("Title cannot be empty".to_string()) + } else { + self.title = title; + self.touch(); + Ok(()) + } + } + + /// Updates the event description. + pub fn update_description(&mut self, description: Option<String>) { + self.description = description; + self.touch(); + } + + /// Updates the recurrence rule. + pub fn update_recurring(&mut self, recurring: Option<RecurrenceRule>) { + self.recurring = recurring; + self.touch(); + } + + /// Reschedules the event to new start and end times. + /// + /// # Errors + /// Returns an error if `start` is not before `end`. + pub fn reschedule( + &mut self, + start: DateTime<Utc>, + end: DateTime<Utc> + ) -> Result<(), String> { + if start >= end { + Err("Start time must be strictly before end time".to_string()) + } else { + self.start = start; + self.end = end; + self.touch(); + Ok(()) + } + } + + /// Updates the color identifier. + pub fn update_color(&mut self, color: u8) { + self.color = color; + self.touch(); + } + + /// Updates whether this is an all-day event. + pub fn update_all_day(&mut self, is_all_day: bool) { + self.is_all_day = is_all_day; + self.touch(); + } + + /// Updates whether this event is cancelled. + pub fn update_is_cancelled(&mut self, is_cancelled: bool) { + self.is_cancelled = is_cancelled; + self.touch(); + } + + /// Returns the duration of the event. + pub fn get_duration(&self) -> TimeDelta { + self.end - self.start + } + + /// Returns true if the event has ended. + pub fn is_past(&self) -> bool { + self.end < Utc::now() + } + + /// Returns true if the event is currently happening. + pub fn is_ongoing(&self) -> bool { + let now = Utc::now(); + self.start <= now && now < self.end + } + + /// Returns true if the event has not yet started. + pub fn is_future(&self) -> bool { + self.start > Utc::now() + } + + /// Updates the `updated_at` timestamp to the current time. + pub fn touch(&mut self) { + self.updated_at = Utc::now(); + } +} diff --git a/src/domain/mod.rs b/src/domain/mod.rs new file mode 100644 index 0000000..f31bc30 --- /dev/null +++ b/src/domain/mod.rs @@ -0,0 +1,6 @@ +pub mod event; +pub mod recurrence; +pub mod calendar; + +pub use event::Event; +pub use recurrence::{RecurrenceRule, Frequency}; diff --git a/src/domain/recurrence.rs b/src/domain/recurrence.rs new file mode 100644 index 0000000..807df4b --- /dev/null +++ b/src/domain/recurrence.rs @@ -0,0 +1,36 @@ +use chrono::{DateTime, Utc}; +use getset::Getters; + +#[derive(Clone, Debug, Getters)] +#[getset(get = "pub")] +pub struct RecurrenceRule { + frequency: Frequency, + interval: u32, + end_date: Option<DateTime<Utc>> +} + +#[derive(Clone, Debug)] +pub enum Frequency { + DAILY, + WEEKLY, + MONTHLY, + YEARLY +} + +impl RecurrenceRule { + pub fn new( + frequency: Frequency, + interval: u32, + end_date: Option<DateTime<Utc>> + ) -> Result<Self, String> { + let now = Utc::now(); + + if let Some(end) = end_date { + if now >= end { + return Err("End date has already passed".to_string()); + } + } + + Ok(Self { frequency, interval, end_date }) + } +} |