//! 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, start: DateTime, end: DateTime, recurring: Option, created_at: DateTime, updated_at: DateTime, 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, end: DateTime, description: Option, recurring: Option, 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, end: DateTime ) -> Result { 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) -> 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) -> 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) { self.description = description; self.touch(); } /// Updates the recurrence rule. pub fn update_recurring(&mut self, recurring: Option) { 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, end: DateTime ) -> 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(); } }