use chrono::{DateTime, Utc}; /** * Calendar Entity * * This module defines the Calendar entity, which represents a calendar in the CalDAV * implementation. Calendars contain calendar events and are owned by users. * * Calendars have properties such as name, color, and description, and they serve as * containers for calendar events. Each calendar belongs to a specific user and can * have custom properties. */ use uuid::Uuid; use crate::common::errors::{DomainError, ErrorKind, Result}; // Re-export entity errors from the centralized module pub use super::entity_errors::CalendarError; /** * Calendar entity. * * Represents a calendar container that can hold multiple calendar events. * Each calendar is owned by a user and has properties like name, color, and description. */ #[derive(Debug, Clone)] pub struct Calendar { /// Unique identifier for the calendar id: Uuid, /// Display name of the calendar name: String, /// ID of the user who owns this calendar owner_id: String, /// Optional description of the calendar description: Option, /// Optional color code for UI display (hex format #RRGGBB) color: Option, /// Time when the calendar was created created_at: DateTime, /// Time when the calendar was last modified updated_at: DateTime, /// Optional list of custom properties (for extended CalDAV support) custom_properties: std::collections::HashMap, } impl Calendar { /** * Creates a new calendar with the given properties. * * @param name Display name of the calendar * @param owner_id ID of the user who owns this calendar * @param description Optional description of the calendar * @param color Optional color code for UI display (#RRGGBB format) * @return Result containing the new Calendar or a domain error */ pub fn new( name: String, owner_id: String, description: Option, color: Option, ) -> Result { // Validate inputs if name.is_empty() { return Err(DomainError::new( ErrorKind::InvalidInput, "Calendar", "Calendar name cannot be empty", )); } if owner_id.is_empty() { return Err(DomainError::new( ErrorKind::InvalidInput, "Calendar", "Owner ID cannot be empty", )); } if let Some(color_str) = &color { Self::validate_color(color_str)?; } let now = Utc::now(); Ok(Self { id: Uuid::new_v4(), name, owner_id, description, color, created_at: now, updated_at: now, custom_properties: std::collections::HashMap::new(), }) } /** * Creates a calendar with specific ID and timestamps. * Typically used when reconstructing from storage. * * @param id Unique identifier for the calendar * @param name Display name of the calendar * @param owner_id ID of the user who owns this calendar * @param description Optional description of the calendar * @param color Optional color code for UI display * @param created_at Time when the calendar was created * @param updated_at Time when the calendar was last modified * @return Result containing the new Calendar or a domain error */ pub fn with_id( id: Uuid, name: String, owner_id: String, description: Option, color: Option, created_at: DateTime, updated_at: DateTime, ) -> Result { // Basic validation if name.is_empty() { return Err(DomainError::new( ErrorKind::InvalidInput, "Calendar", "Calendar name cannot be empty", )); } if owner_id.is_empty() { return Err(DomainError::new( ErrorKind::InvalidInput, "Calendar", "Owner ID cannot be empty", )); } if let Some(color_str) = &color { Self::validate_color(color_str)?; } Ok(Self { id, name, owner_id, description, color, created_at, updated_at, custom_properties: std::collections::HashMap::new(), }) } // Getters /// Returns the calendar's unique identifier pub fn id(&self) -> &Uuid { &self.id } /// Returns the calendar's display name pub fn name(&self) -> &str { &self.name } /// Returns the ID of the user who owns this calendar pub fn owner_id(&self) -> &str { &self.owner_id } /// Returns the calendar's description, if any pub fn description(&self) -> Option<&str> { self.description.as_deref() } /// Returns the calendar's color code, if any pub fn color(&self) -> Option<&str> { self.color.as_deref() } /// Returns the time when the calendar was created pub fn created_at(&self) -> &DateTime { &self.created_at } /// Returns the time when the calendar was last modified pub fn updated_at(&self) -> &DateTime { &self.updated_at } /// Returns a custom property value by name, if it exists pub fn custom_property(&self, name: &str) -> Option<&str> { self.custom_properties.get(name).map(|s| s.as_str()) } /// Returns all custom properties pub fn custom_properties(&self) -> &std::collections::HashMap { &self.custom_properties } // Setters and Mutators /** * Updates the calendar's name. * * @param name New display name for the calendar * @return Result indicating success or containing a domain error */ pub fn update_name(&mut self, name: String) -> Result<()> { if name.is_empty() { return Err(DomainError::new( ErrorKind::InvalidInput, "Calendar", "Calendar name cannot be empty", )); } self.name = name; self.updated_at = Utc::now(); Ok(()) } /** * Updates the calendar's description. * * @param description New description for the calendar */ pub fn update_description(&mut self, description: Option) { self.description = description; self.updated_at = Utc::now(); } /** * Updates the calendar's color. * * @param color New color code for the calendar * @return Result indicating success or containing a domain error */ pub fn update_color(&mut self, color: Option) -> Result<()> { // Validate color format if provided if let Some(color_str) = &color { Self::validate_color(&color_str)?; } self.color = color; self.updated_at = Utc::now(); Ok(()) } /// Validate a calendar color fn validate_color(color: &str) -> Result<()> { if !color.starts_with('#') || !(color.len() == 7 || color.len() == 9) || color[1..].chars().any(|c| !c.is_ascii_hexdigit()) { return Err(DomainError::new( ErrorKind::InvalidInput, "Calendar", "Color must be in #RRGGBB or #RRGGBBAA format", )); } Ok(()) } /** * Sets a custom property for extended CalDAV support. * * @param name Name of the property * @param value Value of the property */ pub fn set_custom_property(&mut self, name: String, value: String) { self.custom_properties.insert(name, value); self.updated_at = Utc::now(); } /** * Removes a custom property. * * @param name Name of the property to remove * @return true if the property was removed, false if it didn't exist */ pub fn remove_custom_property(&mut self, name: &str) -> bool { let result = self.custom_properties.remove(name).is_some(); if result { self.updated_at = Utc::now(); } result } /** * Checks if this calendar belongs to the specified user. * * @param user_id ID of the user to check ownership against * @return true if the calendar belongs to the user, false otherwise */ pub fn belongs_to(&self, user_id: &str) -> bool { self.owner_id == user_id } /** * Updates the last modification time of the calendar to now. * Called when calendar events are added, modified, or removed. */ pub fn touch(&mut self) { self.updated_at = Utc::now(); } } #[cfg(test)] mod tests { use super::*; #[test] fn test_init() { let res = Calendar::new("Name".to_string(), "ID".to_string(), None, None); assert!(res.is_ok()); } #[test] fn test_init_color_rgb() { let res = Calendar::new( "Name".to_string(), "ID".to_string(), None, Some("#84FFa9".to_string()), ); assert!(res.is_ok()); } /// Format as used by the android DAVx app #[test] fn test_init_color_rgba() { let res = Calendar::new( "Name".to_string(), "ID".to_string(), None, Some("#abcdef51".to_string()), ); assert!(res.is_ok()); } #[test] fn test_init_bad_color_1() { let res = Calendar::new( "Name".to_string(), "ID".to_string(), None, Some("foo".to_string()), ); assert!(res.is_err()); } #[test] fn test_init_bad_color_2() { let res = Calendar::new( "Name".to_string(), "ID".to_string(), None, Some("#xxjjff".to_string()), ); assert!(res.is_err()); } }