diff options
Diffstat (limited to 'src/domain/event.rs')
| -rw-r--r-- | src/domain/event.rs | 310 |
1 files changed, 310 insertions, 0 deletions
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(); + } +} |