style: apply cargo fmt to entire codebase

Standardize code formatting across all 173 Rust source files
using rustfmt. No functional changes - purely cosmetic.

This establishes a consistent code style baseline for the
project going forward.
This commit is contained in:
Dionisio
2026-02-14 01:29:34 +01:00
parent 67137a3ef2
commit 4c98c5a657
173 changed files with 23368 additions and 17590 deletions
+50 -51
View File
@@ -1,25 +1,24 @@
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 chrono::{DateTime, Utc};
use crate::common::errors::{Result, DomainError, ErrorKind};
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.
*/
@@ -27,25 +26,25 @@ pub use super::entity_errors::CalendarError;
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<String>,
/// Optional color code for UI display (hex format #RRGGBB)
color: Option<String>,
/// Time when the calendar was created
created_at: DateTime<Utc>,
/// Time when the calendar was last modified
updated_at: DateTime<Utc>,
/// Optional list of custom properties (for extended CalDAV support)
custom_properties: std::collections::HashMap<String, String>,
}
@@ -53,7 +52,7 @@ pub struct Calendar {
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
@@ -74,7 +73,7 @@ impl Calendar {
"Calendar name cannot be empty",
));
}
if owner_id.is_empty() {
return Err(DomainError::new(
ErrorKind::InvalidInput,
@@ -82,7 +81,7 @@ impl Calendar {
"Owner ID cannot be empty",
));
}
// Validate color format if provided (#RRGGBB)
if let Some(ref color_str) = color {
if !color_str.starts_with('#') || color_str.len() != 7 {
@@ -92,7 +91,7 @@ impl Calendar {
"Color must be in #RRGGBB format",
));
}
// Check if remaining characters are valid hex
if color_str[1..].chars().any(|c| !c.is_ascii_hexdigit()) {
return Err(DomainError::new(
@@ -102,9 +101,9 @@ impl Calendar {
));
}
}
let now = Utc::now();
Ok(Self {
id: Uuid::new_v4(),
name,
@@ -116,11 +115,11 @@ impl Calendar {
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
@@ -147,7 +146,7 @@ impl Calendar {
"Calendar name cannot be empty",
));
}
if owner_id.is_empty() {
return Err(DomainError::new(
ErrorKind::InvalidInput,
@@ -155,7 +154,7 @@ impl Calendar {
"Owner ID cannot be empty",
));
}
Ok(Self {
id,
name,
@@ -167,59 +166,59 @@ impl Calendar {
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<Utc> {
&self.created_at
}
/// Returns the time when the calendar was last modified
pub fn updated_at(&self) -> &DateTime<Utc> {
&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<String, String> {
&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
*/
@@ -231,25 +230,25 @@ impl 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<String>) {
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
*/
@@ -263,7 +262,7 @@ impl Calendar {
"Color must be in #RRGGBB format",
));
}
// Check if remaining characters are valid hex
if color_str[1..].chars().any(|c| !c.is_ascii_hexdigit()) {
return Err(DomainError::new(
@@ -273,15 +272,15 @@ impl Calendar {
));
}
}
self.color = color;
self.updated_at = Utc::now();
Ok(())
}
/**
* Sets a custom property for extended CalDAV support.
*
*
* @param name Name of the property
* @param value Value of the property
*/
@@ -289,10 +288,10 @@ impl Calendar {
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
*/
@@ -303,17 +302,17 @@ impl Calendar {
}
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.
@@ -321,4 +320,4 @@ impl Calendar {
pub fn touch(&mut self) {
self.updated_at = Utc::now();
}
}
}
+209 -181
View File
@@ -1,25 +1,24 @@
use chrono::{DateTime, Duration, TimeZone, Utc};
/**
* Calendar Event Entity
*
*
* This module defines the CalendarEvent entity, which represents an event or
* appointment in a calendar, following the iCalendar (RFC 5545) specification.
*
*
* Calendar events have properties like summary, description, location, start/end times,
* and can include recurrence rules for repeating events. Each event belongs to a
* specific calendar and stores its complete iCalendar representation.
*/
use uuid::Uuid;
use chrono::{DateTime, Utc, Duration, TimeZone};
use crate::common::errors::{Result, DomainError, ErrorKind};
use crate::common::errors::{DomainError, ErrorKind, Result};
// Re-export entity errors from the centralized module
pub use super::entity_errors::CalendarEventError;
/**
* CalendarEvent entity.
*
*
* Represents a calendar event or appointment that can be synced via CalDAV.
* Follows the iCalendar format (RFC 5545) for compatibility with CalDAV clients.
*/
@@ -27,40 +26,40 @@ pub use super::entity_errors::CalendarEventError;
pub struct CalendarEvent {
/// Unique identifier for the event
id: Uuid,
/// ID of the calendar this event belongs to
calendar_id: Uuid,
/// Short summary/title of the event
summary: String,
/// Detailed description of the event (optional)
description: Option<String>,
/// Location of the event (optional)
location: Option<String>,
/// Start time of the event
start_time: DateTime<Utc>,
/// End time of the event
end_time: DateTime<Utc>,
/// Whether this is an all-day event
all_day: bool,
/// Recurrence rule in iCalendar RRULE format (optional)
rrule: Option<String>,
/// Unique identifier in iCalendar format (used for CalDAV sync)
ical_uid: String,
/// Complete iCalendar data (VEVENT component)
ical_data: String,
/// Time when the event was created
created_at: DateTime<Utc>,
/// Time when the event was last modified
updated_at: DateTime<Utc>,
}
@@ -68,7 +67,7 @@ pub struct CalendarEvent {
impl CalendarEvent {
/**
* Creates a new calendar event with the given properties.
*
*
* @param calendar_id ID of the calendar this event belongs to
* @param summary Short summary/title of the event
* @param description Detailed description of the event (optional)
@@ -99,7 +98,7 @@ impl CalendarEvent {
"Event summary cannot be empty",
));
}
if end_time < start_time {
return Err(DomainError::new(
ErrorKind::InvalidInput,
@@ -107,17 +106,18 @@ impl CalendarEvent {
"End time cannot be before start time",
));
}
// Validate RRULE if provided (basic validation)
if let Some(ref rule) = rrule
&& !rule.starts_with("FREQ=") {
return Err(DomainError::new(
ErrorKind::InvalidInput,
"CalendarEvent",
"Recurrence rule must start with FREQ=",
));
}
&& !rule.starts_with("FREQ=")
{
return Err(DomainError::new(
ErrorKind::InvalidInput,
"CalendarEvent",
"Recurrence rule must start with FREQ=",
));
}
// Validate iCalendar data (basic validation)
if !ical_data.contains("BEGIN:VEVENT") || !ical_data.contains("END:VEVENT") {
return Err(DomainError::new(
@@ -126,9 +126,9 @@ impl CalendarEvent {
"iCalendar data must contain a VEVENT component",
));
}
let now = Utc::now();
Ok(Self {
id: Uuid::new_v4(),
calendar_id,
@@ -145,11 +145,11 @@ impl CalendarEvent {
updated_at: now,
})
}
/**
* Creates a calendar event with specific ID and timestamps.
* Typically used when reconstructing from storage.
*
*
* @param id Unique identifier for the event
* @param calendar_id ID of the calendar this event belongs to
* @param summary Short summary/title of the event
@@ -188,7 +188,7 @@ impl CalendarEvent {
"Event summary cannot be empty",
));
}
if end_time < start_time {
return Err(DomainError::new(
ErrorKind::InvalidInput,
@@ -196,7 +196,7 @@ impl CalendarEvent {
"End time cannot be before start time",
));
}
Ok(Self {
id,
calendar_id,
@@ -213,11 +213,11 @@ impl CalendarEvent {
updated_at,
})
}
/**
* Creates a calendar event from an iCalendar VEVENT component.
* Parses the iCalendar data to extract event properties.
*
*
* @param calendar_id ID of the calendar this event belongs to
* @param ical_data Complete iCalendar data (VEVENT component)
* @return Result containing the new CalendarEvent or a domain error
@@ -225,58 +225,63 @@ impl CalendarEvent {
pub fn from_ical(calendar_id: Uuid, ical_data: String) -> Result<Self> {
// This implementation would require a proper iCalendar parser
// For brevity, we're using a simplified version here
// Extract required fields from iCalendar data
let summary = Self::extract_ical_property(&ical_data, "SUMMARY")
.ok_or_else(|| DomainError::new(
let summary = Self::extract_ical_property(&ical_data, "SUMMARY").ok_or_else(|| {
DomainError::new(
ErrorKind::InvalidInput,
"CalendarEvent",
"Missing SUMMARY in iCalendar data",
))?;
let dtstart = Self::extract_ical_property(&ical_data, "DTSTART")
.ok_or_else(|| DomainError::new(
)
})?;
let dtstart = Self::extract_ical_property(&ical_data, "DTSTART").ok_or_else(|| {
DomainError::new(
ErrorKind::InvalidInput,
"CalendarEvent",
"Missing DTSTART in iCalendar data",
))?;
let dtend = Self::extract_ical_property(&ical_data, "DTEND")
.ok_or_else(|| DomainError::new(
)
})?;
let dtend = Self::extract_ical_property(&ical_data, "DTEND").ok_or_else(|| {
DomainError::new(
ErrorKind::InvalidInput,
"CalendarEvent",
"Missing DTEND in iCalendar data",
))?;
)
})?;
// Parse dates (simplified)
let start_time = Self::parse_ical_datetime(&dtstart)
.map_err(|e| DomainError::new(
let start_time = Self::parse_ical_datetime(&dtstart).map_err(|e| {
DomainError::new(
ErrorKind::InvalidInput,
"CalendarEvent",
format!("Invalid DTSTART: {}", e),
))?;
let end_time = Self::parse_ical_datetime(&dtend)
.map_err(|e| DomainError::new(
)
})?;
let end_time = Self::parse_ical_datetime(&dtend).map_err(|e| {
DomainError::new(
ErrorKind::InvalidInput,
"CalendarEvent",
format!("Invalid DTEND: {}", e),
))?;
)
})?;
// Determine if all-day event (simplified check)
let all_day = dtstart.contains("VALUE=DATE") && !dtstart.contains("T");
// Extract optional fields
let description = Self::extract_ical_property(&ical_data, "DESCRIPTION");
let location = Self::extract_ical_property(&ical_data, "LOCATION");
let rrule = Self::extract_ical_property(&ical_data, "RRULE");
// Extract UID or generate a new one
let ical_uid = Self::extract_ical_property(&ical_data, "UID")
.unwrap_or_else(|| Uuid::new_v4().to_string());
let now = Utc::now();
Ok(Self {
id: Uuid::new_v4(),
calendar_id,
@@ -293,84 +298,84 @@ impl CalendarEvent {
updated_at: now,
})
}
// Getters
/// Returns the event's unique identifier
pub fn id(&self) -> &Uuid {
&self.id
}
/// Returns the ID of the calendar this event belongs to
pub fn calendar_id(&self) -> &Uuid {
&self.calendar_id
}
/// Returns the event's summary/title
pub fn summary(&self) -> &str {
&self.summary
}
/// Returns the event's description, if any
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
/// Returns the event's location, if any
pub fn location(&self) -> Option<&str> {
self.location.as_deref()
}
/// Returns the event's start time
pub fn start_time(&self) -> &DateTime<Utc> {
&self.start_time
}
/// Returns the event's end time
pub fn end_time(&self) -> &DateTime<Utc> {
&self.end_time
}
/// Returns whether this is an all-day event
pub fn all_day(&self) -> bool {
self.all_day
}
/// Returns the event's recurrence rule, if any
pub fn rrule(&self) -> Option<&str> {
self.rrule.as_deref()
}
/// Returns the event's iCalendar UID
pub fn ical_uid(&self) -> &str {
&self.ical_uid
}
/// Returns the complete iCalendar data for the event
pub fn ical_data(&self) -> &str {
&self.ical_data
}
/// Returns the time when the event was created
pub fn created_at(&self) -> &DateTime<Utc> {
&self.created_at
}
/// Returns the time when the event was last modified
pub fn updated_at(&self) -> &DateTime<Utc> {
&self.updated_at
}
/// Returns the duration of the event
pub fn duration(&self) -> Duration {
self.end_time - self.start_time
}
// Setters and Mutators
/**
* Updates the event's summary/title.
*
*
* @param summary New summary/title for the event
* @return Result indicating success or containing a domain error
*/
@@ -382,58 +387,62 @@ impl CalendarEvent {
"Event summary cannot be empty",
));
}
// Clone the summary before updating the struct
let summary_clone = summary.clone();
self.summary = summary;
self.updated_at = Utc::now();
// Update iCalendar data using the cloned value
self.update_ical_property("SUMMARY", &summary_clone);
Ok(())
}
/**
* Updates the event's description.
*
*
* @param description New description for the event
*/
pub fn update_description(&mut self, description: Option<String>) {
self.description = description.clone();
self.updated_at = Utc::now();
// Update iCalendar data
match description {
Some(desc) => self.update_ical_property("DESCRIPTION", &desc),
None => self.remove_ical_property("DESCRIPTION"),
}
}
/**
* Updates the event's location.
*
*
* @param location New location for the event
*/
pub fn update_location(&mut self, location: Option<String>) {
self.location = location.clone();
self.updated_at = Utc::now();
// Update iCalendar data
match location {
Some(loc) => self.update_ical_property("LOCATION", &loc),
None => self.remove_ical_property("LOCATION"),
}
}
/**
* Updates the event's start and end times.
*
*
* @param start_time New start time for the event
* @param end_time New end time for the event
* @return Result indicating success or containing a domain error
*/
pub fn update_time_range(&mut self, start_time: DateTime<Utc>, end_time: DateTime<Utc>) -> Result<()> {
pub fn update_time_range(
&mut self,
start_time: DateTime<Utc>,
end_time: DateTime<Utc>,
) -> Result<()> {
if end_time < start_time {
return Err(DomainError::new(
ErrorKind::InvalidInput,
@@ -441,89 +450,90 @@ impl CalendarEvent {
"End time cannot be before start time",
));
}
self.start_time = start_time;
self.end_time = end_time;
self.updated_at = Utc::now();
// Update iCalendar data
let start_str = if self.all_day {
format!("{}T000000Z", start_time.format("%Y%m%d"))
} else {
format!("{}", start_time.format("%Y%m%dT%H%M%SZ"))
};
let end_str = if self.all_day {
format!("{}T000000Z", end_time.format("%Y%m%d"))
} else {
format!("{}", end_time.format("%Y%m%dT%H%M%SZ"))
};
self.update_ical_property("DTSTART", &start_str);
self.update_ical_property("DTEND", &end_str);
Ok(())
}
/**
* Updates whether this is an all-day event.
*
*
* @param all_day Whether this is an all-day event
*/
pub fn update_all_day(&mut self, all_day: bool) {
self.all_day = all_day;
self.updated_at = Utc::now();
// Update iCalendar data
let start_str = if all_day {
format!("VALUE=DATE:{}", self.start_time.format("%Y%m%d"))
} else {
format!("{}", self.start_time.format("%Y%m%dT%H%M%SZ"))
};
let end_str = if all_day {
format!("VALUE=DATE:{}", self.end_time.format("%Y%m%d"))
} else {
format!("{}", self.end_time.format("%Y%m%dT%H%M%SZ"))
};
self.update_ical_property("DTSTART", &start_str);
self.update_ical_property("DTEND", &end_str);
}
/**
* Updates the event's recurrence rule.
*
*
* @param rrule New recurrence rule for the event
* @return Result indicating success or containing a domain error
*/
pub fn update_rrule(&mut self, rrule: Option<String>) -> Result<()> {
// Validate RRULE if provided (basic validation)
if let Some(ref rule) = rrule
&& !rule.starts_with("FREQ=") {
return Err(DomainError::new(
ErrorKind::InvalidInput,
"CalendarEvent",
"Recurrence rule must start with FREQ=",
));
}
&& !rule.starts_with("FREQ=")
{
return Err(DomainError::new(
ErrorKind::InvalidInput,
"CalendarEvent",
"Recurrence rule must start with FREQ=",
));
}
self.rrule = rrule.clone();
self.updated_at = Utc::now();
// Update iCalendar data
match rrule {
Some(rule) => self.update_ical_property("RRULE", &rule),
None => self.remove_ical_property("RRULE"),
}
Ok(())
}
/**
* Updates the complete iCalendar data for the event.
* Also updates the event properties based on the new iCalendar data.
*
*
* @param ical_data New iCalendar data for the event
* @return Result indicating success or containing a domain error
*/
@@ -536,55 +546,57 @@ impl CalendarEvent {
"iCalendar data must contain a VEVENT component",
));
}
// Extract and update properties from iCalendar data
if let Some(summary) = Self::extract_ical_property(&ical_data, "SUMMARY") {
self.summary = summary;
}
self.description = Self::extract_ical_property(&ical_data, "DESCRIPTION");
self.location = Self::extract_ical_property(&ical_data, "LOCATION");
if let Some(dtstart) = Self::extract_ical_property(&ical_data, "DTSTART")
&& let Ok(start_time) = Self::parse_ical_datetime(&dtstart) {
self.start_time = start_time;
}
&& let Ok(start_time) = Self::parse_ical_datetime(&dtstart)
{
self.start_time = start_time;
}
if let Some(dtend) = Self::extract_ical_property(&ical_data, "DTEND")
&& let Ok(end_time) = Self::parse_ical_datetime(&dtend) {
self.end_time = end_time;
}
&& let Ok(end_time) = Self::parse_ical_datetime(&dtend)
{
self.end_time = end_time;
}
// Update all-day status based on DTSTART
if let Some(dtstart) = Self::extract_ical_property(&ical_data, "DTSTART") {
self.all_day = dtstart.contains("VALUE=DATE") && !dtstart.contains("T");
}
self.rrule = Self::extract_ical_property(&ical_data, "RRULE");
if let Some(uid) = Self::extract_ical_property(&ical_data, "UID") {
self.ical_uid = uid;
}
self.ical_data = ical_data;
self.updated_at = Utc::now();
Ok(())
}
/**
* Checks if this event belongs to the specified calendar.
*
*
* @param calendar_id ID of the calendar to check against
* @return true if the event belongs to the calendar, false otherwise
*/
pub fn belongs_to_calendar(&self, calendar_id: &Uuid) -> bool {
self.calendar_id == *calendar_id
}
/**
* Checks if this event occurs within the specified time range.
*
*
* @param start Start of the time range to check
* @param end End of the time range to check
* @return true if the event occurs within the range, false otherwise
@@ -594,20 +606,20 @@ impl CalendarEvent {
if self.start_time <= *end && self.end_time >= *start {
return true;
}
// If event has recurrence, check if any recurrence occurs in range
// Note: A full implementation would need a proper recurrence rule parser
if let Some(rrule) = &self.rrule {
// Simplified check for demonstration
// A real implementation would need to generate recurrence instances
// and check if any fall within the range
// For now, we'll just check if the recurrence hasn't ended
// or if it ended after the start of our range
if let Some(until_pos) = rrule.find("UNTIL=") {
let until_start = until_pos + 6; // "UNTIL=" is 6 chars
if let Some(until_end) = rrule[until_start..].find(';') {
let until_str = &rrule[until_start..until_start+until_end];
let until_str = &rrule[until_start..until_start + until_end];
if let Ok(until_date) = Self::parse_ical_datetime(until_str) {
return until_date >= *start;
}
@@ -623,15 +635,15 @@ impl CalendarEvent {
return true;
}
}
false
}
// Helper methods for iCalendar operations
/**
* Extracts a property value from iCalendar data.
*
*
* @param ical_data The iCalendar data to search in
* @param property_name The name of the property to extract
* @return Option containing the property value if found
@@ -640,33 +652,34 @@ impl CalendarEvent {
// Find the property in the iCalendar data
let search_str = format!("\n{}:", property_name);
let search_str_alt = format!("\r\n{}:", property_name);
let pos = ical_data.find(&search_str)
let pos = ical_data
.find(&search_str)
.or_else(|| ical_data.find(&search_str_alt));
if let Some(pos) = pos {
// Find the start of the value
let value_start = pos + search_str.len();
// Find the end of the value (next line or end of string)
let value_end = ical_data[value_start..]
.find('\n')
.map(|p| value_start + p)
.unwrap_or_else(|| ical_data.len());
// Extract and return the value
let value = ical_data[value_start..value_end].trim();
if !value.is_empty() {
return Some(value.to_string());
}
}
None
}
/**
* Parses an iCalendar datetime string into a DateTime object.
*
*
* @param datetime The iCalendar datetime string to parse
* @return Result containing the parsed DateTime or an error
*/
@@ -677,40 +690,49 @@ impl CalendarEvent {
if date_str.len() != 8 {
return Err("Invalid date format".to_string());
}
let year = date_str[0..4].parse::<i32>()
let year = date_str[0..4]
.parse::<i32>()
.map_err(|_| "Invalid year".to_string())?;
let month = date_str[4..6].parse::<u32>()
let month = date_str[4..6]
.parse::<u32>()
.map_err(|_| "Invalid month".to_string())?;
let day = date_str[6..8].parse::<u32>()
let day = date_str[6..8]
.parse::<u32>()
.map_err(|_| "Invalid day".to_string())?;
return match chrono::NaiveDate::from_ymd_opt(year, month, day) {
Some(date) => Ok(Utc.from_utc_datetime(&date.and_hms_opt(0, 0, 0).unwrap())),
None => Err("Invalid date components".to_string()),
};
}
// Handle standard UTC format (20230101T120000Z)
let datetime_str = datetime.split(':').next_back().unwrap_or(datetime);
if datetime_str.len() < 15 || !datetime_str.ends_with('Z') {
return Err("Invalid datetime format".to_string());
}
let year = datetime_str[0..4].parse::<i32>()
let year = datetime_str[0..4]
.parse::<i32>()
.map_err(|_| "Invalid year".to_string())?;
let month = datetime_str[4..6].parse::<u32>()
let month = datetime_str[4..6]
.parse::<u32>()
.map_err(|_| "Invalid month".to_string())?;
let day = datetime_str[6..8].parse::<u32>()
let day = datetime_str[6..8]
.parse::<u32>()
.map_err(|_| "Invalid day".to_string())?;
let hour = datetime_str[9..11].parse::<u32>()
let hour = datetime_str[9..11]
.parse::<u32>()
.map_err(|_| "Invalid hour".to_string())?;
let minute = datetime_str[11..13].parse::<u32>()
let minute = datetime_str[11..13]
.parse::<u32>()
.map_err(|_| "Invalid minute".to_string())?;
let second = datetime_str[13..15].parse::<u32>()
let second = datetime_str[13..15]
.parse::<u32>()
.map_err(|_| "Invalid second".to_string())?;
match chrono::NaiveDate::from_ymd_opt(year, month, day) {
Some(date) => match date.and_hms_opt(hour, minute, second) {
Some(datetime) => Ok(Utc.from_utc_datetime(&datetime)),
@@ -719,70 +741,76 @@ impl CalendarEvent {
None => Err("Invalid date components".to_string()),
}
}
/**
* Updates an iCalendar property in the event's iCalendar data.
*
*
* @param property_name The name of the property to update
* @param value The new value for the property
*/
fn update_ical_property(&mut self, property_name: &str, value: &str) {
let search_str = format!("\n{}:", property_name);
let search_str_alt = format!("\r\n{}:", property_name);
// Check if property exists
let pos = self.ical_data.find(&search_str)
let pos = self
.ical_data
.find(&search_str)
.or_else(|| self.ical_data.find(&search_str_alt));
if let Some(pos) = pos {
// Find the start of the value
let value_start = pos + search_str.len();
// Find the end of the value (next line or end of string)
let value_end = self.ical_data[value_start..]
.find('\n')
.map(|p| value_start + p)
.unwrap_or_else(|| self.ical_data.len());
// Replace the value
let before = &self.ical_data[..value_start];
let after = &self.ical_data[value_end..];
self.ical_data = format!("{}{}{}", before, value, after);
} else {
// Property doesn't exist, add it before END:VEVENT
let end_pos = self.ical_data.find("END:VEVENT")
let end_pos = self
.ical_data
.find("END:VEVENT")
.unwrap_or(self.ical_data.len());
let before = &self.ical_data[..end_pos];
let after = &self.ical_data[end_pos..];
self.ical_data = format!("{}{}:{}\n{}", before, property_name, value, after);
}
}
/**
* Removes an iCalendar property from the event's iCalendar data.
*
*
* @param property_name The name of the property to remove
*/
fn remove_ical_property(&mut self, property_name: &str) {
let search_str = format!("\n{}:", property_name);
let search_str_alt = format!("\r\n{}:", property_name);
// Check if property exists
let pos = self.ical_data.find(&search_str)
let pos = self
.ical_data
.find(&search_str)
.or_else(|| self.ical_data.find(&search_str_alt));
if let Some(pos) = pos {
// Find the end of the value (next line or end of string)
let value_end = self.ical_data[pos + 1..]
.find('\n')
.map(|p| pos + 1 + p)
.unwrap_or_else(|| self.ical_data.len());
// Remove the property
let before = &self.ical_data[..pos];
let after = &self.ical_data[value_end..];
self.ical_data = format!("{}{}", before, after);
}
}
}
}
+263 -78
View File
@@ -46,25 +46,64 @@ impl AddressBook {
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
) -> Self {
Self { id, name, owner_id, description, color, is_public, created_at, updated_at }
Self {
id,
name,
owner_id,
description,
color,
is_public,
created_at,
updated_at,
}
}
// --- Getters ---
pub fn id(&self) -> &Uuid { &self.id }
pub fn name(&self) -> &str { &self.name }
pub fn owner_id(&self) -> &str { &self.owner_id }
pub fn description(&self) -> Option<&str> { self.description.as_deref() }
pub fn color(&self) -> Option<&str> { self.color.as_deref() }
pub fn is_public(&self) -> bool { self.is_public }
pub fn created_at(&self) -> &DateTime<Utc> { &self.created_at }
pub fn updated_at(&self) -> &DateTime<Utc> { &self.updated_at }
pub fn id(&self) -> &Uuid {
&self.id
}
pub fn name(&self) -> &str {
&self.name
}
pub fn owner_id(&self) -> &str {
&self.owner_id
}
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
pub fn color(&self) -> Option<&str> {
self.color.as_deref()
}
pub fn is_public(&self) -> bool {
self.is_public
}
pub fn created_at(&self) -> &DateTime<Utc> {
&self.created_at
}
pub fn updated_at(&self) -> &DateTime<Utc> {
&self.updated_at
}
// --- Setters for mutable operations ---
pub fn set_name(&mut self, name: String) { self.name = name; self.updated_at = Utc::now(); }
pub fn set_description(&mut self, description: Option<String>) { self.description = description; self.updated_at = Utc::now(); }
pub fn set_color(&mut self, color: Option<String>) { self.color = color; self.updated_at = Utc::now(); }
pub fn set_is_public(&mut self, is_public: bool) { self.is_public = is_public; self.updated_at = Utc::now(); }
pub fn set_updated_at(&mut self, updated_at: DateTime<Utc>) { self.updated_at = updated_at; }
pub fn set_name(&mut self, name: String) {
self.name = name;
self.updated_at = Utc::now();
}
pub fn set_description(&mut self, description: Option<String>) {
self.description = description;
self.updated_at = Utc::now();
}
pub fn set_color(&mut self, color: Option<String>) {
self.color = color;
self.updated_at = Utc::now();
}
pub fn set_is_public(&mut self, is_public: bool) {
self.is_public = is_public;
self.updated_at = Utc::now();
}
pub fn set_updated_at(&mut self, updated_at: DateTime<Utc>) {
self.updated_at = updated_at;
}
}
impl Default for AddressBook {
@@ -198,72 +237,191 @@ impl Contact {
updated_at: DateTime<Utc>,
) -> Self {
Self {
id, address_book_id, uid, full_name, first_name, last_name, nickname,
email, phone, address, organization, title, notes, photo_url,
birthday, anniversary, vcard, etag, created_at, updated_at,
id,
address_book_id,
uid,
full_name,
first_name,
last_name,
nickname,
email,
phone,
address,
organization,
title,
notes,
photo_url,
birthday,
anniversary,
vcard,
etag,
created_at,
updated_at,
}
}
// --- Getters ---
pub fn id(&self) -> &Uuid { &self.id }
pub fn address_book_id(&self) -> &Uuid { &self.address_book_id }
pub fn uid(&self) -> &str { &self.uid }
pub fn full_name(&self) -> Option<&str> { self.full_name.as_deref() }
pub fn first_name(&self) -> Option<&str> { self.first_name.as_deref() }
pub fn last_name(&self) -> Option<&str> { self.last_name.as_deref() }
pub fn nickname(&self) -> Option<&str> { self.nickname.as_deref() }
pub fn email(&self) -> &[Email] { &self.email }
pub fn phone(&self) -> &[Phone] { &self.phone }
pub fn address(&self) -> &[Address] { &self.address }
pub fn organization(&self) -> Option<&str> { self.organization.as_deref() }
pub fn title(&self) -> Option<&str> { self.title.as_deref() }
pub fn notes(&self) -> Option<&str> { self.notes.as_deref() }
pub fn photo_url(&self) -> Option<&str> { self.photo_url.as_deref() }
pub fn birthday(&self) -> Option<&NaiveDate> { self.birthday.as_ref() }
pub fn anniversary(&self) -> Option<&NaiveDate> { self.anniversary.as_ref() }
pub fn vcard(&self) -> &str { &self.vcard }
pub fn etag(&self) -> &str { &self.etag }
pub fn created_at(&self) -> &DateTime<Utc> { &self.created_at }
pub fn updated_at(&self) -> &DateTime<Utc> { &self.updated_at }
pub fn id(&self) -> &Uuid {
&self.id
}
pub fn address_book_id(&self) -> &Uuid {
&self.address_book_id
}
pub fn uid(&self) -> &str {
&self.uid
}
pub fn full_name(&self) -> Option<&str> {
self.full_name.as_deref()
}
pub fn first_name(&self) -> Option<&str> {
self.first_name.as_deref()
}
pub fn last_name(&self) -> Option<&str> {
self.last_name.as_deref()
}
pub fn nickname(&self) -> Option<&str> {
self.nickname.as_deref()
}
pub fn email(&self) -> &[Email] {
&self.email
}
pub fn phone(&self) -> &[Phone] {
&self.phone
}
pub fn address(&self) -> &[Address] {
&self.address
}
pub fn organization(&self) -> Option<&str> {
self.organization.as_deref()
}
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
pub fn notes(&self) -> Option<&str> {
self.notes.as_deref()
}
pub fn photo_url(&self) -> Option<&str> {
self.photo_url.as_deref()
}
pub fn birthday(&self) -> Option<&NaiveDate> {
self.birthday.as_ref()
}
pub fn anniversary(&self) -> Option<&NaiveDate> {
self.anniversary.as_ref()
}
pub fn vcard(&self) -> &str {
&self.vcard
}
pub fn etag(&self) -> &str {
&self.etag
}
pub fn created_at(&self) -> &DateTime<Utc> {
&self.created_at
}
pub fn updated_at(&self) -> &DateTime<Utc> {
&self.updated_at
}
// --- Owned getters for persistence layer bind() calls ---
pub fn full_name_owned(&self) -> Option<String> { self.full_name.clone() }
pub fn first_name_owned(&self) -> Option<String> { self.first_name.clone() }
pub fn last_name_owned(&self) -> Option<String> { self.last_name.clone() }
pub fn nickname_owned(&self) -> Option<String> { self.nickname.clone() }
pub fn organization_owned(&self) -> Option<String> { self.organization.clone() }
pub fn title_owned(&self) -> Option<String> { self.title.clone() }
pub fn notes_owned(&self) -> Option<String> { self.notes.clone() }
pub fn photo_url_owned(&self) -> Option<String> { self.photo_url.clone() }
pub fn full_name_owned(&self) -> Option<String> {
self.full_name.clone()
}
pub fn first_name_owned(&self) -> Option<String> {
self.first_name.clone()
}
pub fn last_name_owned(&self) -> Option<String> {
self.last_name.clone()
}
pub fn nickname_owned(&self) -> Option<String> {
self.nickname.clone()
}
pub fn organization_owned(&self) -> Option<String> {
self.organization.clone()
}
pub fn title_owned(&self) -> Option<String> {
self.title.clone()
}
pub fn notes_owned(&self) -> Option<String> {
self.notes.clone()
}
pub fn photo_url_owned(&self) -> Option<String> {
self.photo_url.clone()
}
// --- Setters for mutable operations (contact_service.rs needs these) ---
pub fn set_full_name(&mut self, v: Option<String>) { self.full_name = v; }
pub fn set_first_name(&mut self, v: Option<String>) { self.first_name = v; }
pub fn set_last_name(&mut self, v: Option<String>) { self.last_name = v; }
pub fn set_nickname(&mut self, v: Option<String>) { self.nickname = v; }
pub fn set_organization(&mut self, v: Option<String>) { self.organization = v; }
pub fn set_title(&mut self, v: Option<String>) { self.title = v; }
pub fn set_notes(&mut self, v: Option<String>) { self.notes = v; }
pub fn set_photo_url(&mut self, v: Option<String>) { self.photo_url = v; }
pub fn set_birthday(&mut self, v: Option<NaiveDate>) { self.birthday = v; }
pub fn set_anniversary(&mut self, v: Option<NaiveDate>) { self.anniversary = v; }
pub fn set_vcard(&mut self, vcard: String) { self.vcard = vcard; }
pub fn set_etag(&mut self, etag: String) { self.etag = etag; }
pub fn set_updated_at(&mut self, updated_at: DateTime<Utc>) { self.updated_at = updated_at; }
pub fn set_address_book_id(&mut self, id: Uuid) { self.address_book_id = id; }
pub fn set_uid(&mut self, uid: String) { self.uid = uid; }
pub fn set_full_name(&mut self, v: Option<String>) {
self.full_name = v;
}
pub fn set_first_name(&mut self, v: Option<String>) {
self.first_name = v;
}
pub fn set_last_name(&mut self, v: Option<String>) {
self.last_name = v;
}
pub fn set_nickname(&mut self, v: Option<String>) {
self.nickname = v;
}
pub fn set_organization(&mut self, v: Option<String>) {
self.organization = v;
}
pub fn set_title(&mut self, v: Option<String>) {
self.title = v;
}
pub fn set_notes(&mut self, v: Option<String>) {
self.notes = v;
}
pub fn set_photo_url(&mut self, v: Option<String>) {
self.photo_url = v;
}
pub fn set_birthday(&mut self, v: Option<NaiveDate>) {
self.birthday = v;
}
pub fn set_anniversary(&mut self, v: Option<NaiveDate>) {
self.anniversary = v;
}
pub fn set_vcard(&mut self, vcard: String) {
self.vcard = vcard;
}
pub fn set_etag(&mut self, etag: String) {
self.etag = etag;
}
pub fn set_updated_at(&mut self, updated_at: DateTime<Utc>) {
self.updated_at = updated_at;
}
pub fn set_address_book_id(&mut self, id: Uuid) {
self.address_book_id = id;
}
pub fn set_uid(&mut self, uid: String) {
self.uid = uid;
}
// --- Collection mutators ---
pub fn push_email(&mut self, e: Email) { self.email.push(e); }
pub fn push_phone(&mut self, p: Phone) { self.phone.push(p); }
pub fn set_email(&mut self, email: Vec<Email>) { self.email = email; }
pub fn set_phone(&mut self, phone: Vec<Phone>) { self.phone = phone; }
pub fn set_address(&mut self, address: Vec<Address>) { self.address = address; }
pub fn email_is_empty(&self) -> bool { self.email.is_empty() }
pub fn phone_is_empty(&self) -> bool { self.phone.is_empty() }
pub fn push_email(&mut self, e: Email) {
self.email.push(e);
}
pub fn push_phone(&mut self, p: Phone) {
self.phone.push(p);
}
pub fn set_email(&mut self, email: Vec<Email>) {
self.email = email;
}
pub fn set_phone(&mut self, phone: Vec<Phone>) {
self.phone = phone;
}
pub fn set_address(&mut self, address: Vec<Address>) {
self.address = address;
}
pub fn email_is_empty(&self) -> bool {
self.email.is_empty()
}
pub fn phone_is_empty(&self) -> bool {
self.phone.is_empty()
}
// --- Consuming methods for ownership transfer ---
pub fn into_email(self) -> Vec<Email> { self.email }
pub fn into_email(self) -> Vec<Email> {
self.email
}
pub fn into_parts(self) -> ContactParts {
ContactParts {
id: self.id,
@@ -355,7 +513,13 @@ impl ContactGroup {
/// Creates a new ContactGroup with generated id and timestamps
pub fn new(address_book_id: Uuid, name: String) -> Self {
let now = Utc::now();
Self { id: Uuid::new_v4(), address_book_id, name, created_at: now, updated_at: now }
Self {
id: Uuid::new_v4(),
address_book_id,
name,
created_at: now,
updated_at: now,
}
}
/// Reconstructs from persistence
@@ -366,23 +530,44 @@ impl ContactGroup {
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
) -> Self {
Self { id, address_book_id, name, created_at, updated_at }
Self {
id,
address_book_id,
name,
created_at,
updated_at,
}
}
// --- Getters ---
pub fn id(&self) -> &Uuid { &self.id }
pub fn address_book_id(&self) -> &Uuid { &self.address_book_id }
pub fn name(&self) -> &str { &self.name }
pub fn created_at(&self) -> &DateTime<Utc> { &self.created_at }
pub fn updated_at(&self) -> &DateTime<Utc> { &self.updated_at }
pub fn id(&self) -> &Uuid {
&self.id
}
pub fn address_book_id(&self) -> &Uuid {
&self.address_book_id
}
pub fn name(&self) -> &str {
&self.name
}
pub fn created_at(&self) -> &DateTime<Utc> {
&self.created_at
}
pub fn updated_at(&self) -> &DateTime<Utc> {
&self.updated_at
}
// --- Setters ---
pub fn set_name(&mut self, name: String) { self.name = name; self.updated_at = Utc::now(); }
pub fn set_updated_at(&mut self, updated_at: DateTime<Utc>) { self.updated_at = updated_at; }
pub fn set_name(&mut self, name: String) {
self.name = name;
self.updated_at = Utc::now();
}
pub fn set_updated_at(&mut self, updated_at: DateTime<Utc>) {
self.updated_at = updated_at;
}
}
impl Default for ContactGroup {
fn default() -> Self {
ContactGroup::new(Uuid::new_v4(), "New Group".to_string())
}
}
}
+258 -254
View File
@@ -1,254 +1,258 @@
//! Pure domain entity errors
//!
//! This module defines domain entity-specific errors
//! without external framework dependencies, following
//! Clean Architecture principles.
//!
//! Errors manually implement `std::error::Error` and `std::fmt::Display`
//! to keep the domain free of external dependencies.
use std::error::Error;
use std::fmt::{Display, Formatter, Result as FmtResult};
// ============================================================================
// FILE ERRORS
// ============================================================================
/// Errors that can occur during File entity operations
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileError {
/// Occurs when the file name contains invalid characters or is empty
InvalidFileName(String),
/// Occurs when validation of any entity attribute fails
ValidationError(String),
}
impl Display for FileError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
FileError::InvalidFileName(name) => write!(f, "Invalid file name: {}", name),
FileError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
}
}
}
impl Error for FileError {}
/// Type alias for File entity operation results
pub type FileResult<T> = Result<T, FileError>;
// ============================================================================
// FOLDER ERRORS
// ============================================================================
/// Errors that can occur during Folder entity operations
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FolderError {
/// Occurs when the folder name contains invalid characters or is empty
InvalidFolderName(String),
/// Occurs when validation of any entity attribute fails
ValidationError(String),
}
impl Display for FolderError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
FolderError::InvalidFolderName(name) => write!(f, "Invalid folder name: {}", name),
FolderError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
}
}
}
impl Error for FolderError {}
/// Type alias for Folder entity operation results
pub type FolderResult<T> = Result<T, FolderError>;
// ============================================================================
// USER ERRORS
// ============================================================================
/// Errors that can occur during User entity operations
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UserError {
/// Invalid username
InvalidUsername(String),
/// Invalid password
InvalidPassword(String),
/// General validation error
ValidationError(String),
/// Authentication error
AuthenticationError(String),
}
impl Display for UserError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
UserError::InvalidUsername(msg) => write!(f, "Invalid username: {}", msg),
UserError::InvalidPassword(msg) => write!(f, "Invalid password: {}", msg),
UserError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
UserError::AuthenticationError(msg) => write!(f, "Authentication error: {}", msg),
}
}
}
impl Error for UserError {}
/// Type alias for User entity operation results
pub type UserResult<T> = Result<T, UserError>;
// ============================================================================
// SHARE ERRORS
// ============================================================================
/// Errors that can occur during Share entity operations
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ShareError {
/// Invalid share token
InvalidToken(String),
/// Invalid expiration date
InvalidExpiration(String),
/// General validation error
ValidationError(String),
}
impl Display for ShareError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
ShareError::InvalidToken(msg) => write!(f, "Invalid token: {}", msg),
ShareError::InvalidExpiration(msg) => write!(f, "Invalid expiration date: {}", msg),
ShareError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
}
}
}
impl Error for ShareError {}
/// Type alias for Share entity operation results
pub type ShareResult<T> = Result<T, ShareError>;
// ============================================================================
// CALENDAR ERRORS
// ============================================================================
/// Errors that can occur during Calendar entity operations
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CalendarError {
/// Invalid calendar name
InvalidName(String),
/// Invalid color code
InvalidColor(String),
/// Invalid owner ID
InvalidOwnerId(String),
}
impl Display for CalendarError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
CalendarError::InvalidName(msg) => write!(f, "Invalid calendar name: {}", msg),
CalendarError::InvalidColor(msg) => write!(f, "Invalid color code: {}", msg),
CalendarError::InvalidOwnerId(msg) => write!(f, "Invalid owner ID: {}", msg),
}
}
}
impl Error for CalendarError {}
/// Type alias for Calendar entity operation results
pub type CalendarResult<T> = Result<T, CalendarError>;
// ============================================================================
// CALENDAR EVENT ERRORS
// ============================================================================
/// Errors that can occur during CalendarEvent entity operations
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CalendarEventError {
/// Invalid event summary/title
InvalidSummary(String),
/// Invalid event dates
InvalidDates(String),
/// Invalid recurrence rule
InvalidRecurrence(String),
/// Invalid iCalendar data
InvalidICalData(String),
}
impl Display for CalendarEventError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
CalendarEventError::InvalidSummary(msg) => write!(f, "Invalid event summary: {}", msg),
CalendarEventError::InvalidDates(msg) => write!(f, "Invalid event dates: {}", msg),
CalendarEventError::InvalidRecurrence(msg) => write!(f, "Invalid recurrence rule: {}", msg),
CalendarEventError::InvalidICalData(msg) => write!(f, "Invalid iCalendar data: {}", msg),
}
}
}
impl Error for CalendarEventError {}
/// Type alias for CalendarEvent entity operation results
pub type CalendarEventResult<T> = Result<T, CalendarEventError>;
// ============================================================================
// TESTS
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_error_display() {
let err = FileError::InvalidFileName("test.txt".to_string());
assert_eq!(err.to_string(), "Invalid file name: test.txt");
let err = FileError::ValidationError("size too large".to_string());
assert_eq!(err.to_string(), "Validation error: size too large");
}
#[test]
fn test_folder_error_display() {
let err = FolderError::InvalidFolderName("my/folder".to_string());
assert_eq!(err.to_string(), "Invalid folder name: my/folder");
}
#[test]
fn test_user_error_display() {
let err = UserError::InvalidUsername("".to_string());
assert_eq!(err.to_string(), "Invalid username: ");
let err = UserError::AuthenticationError("invalid credentials".to_string());
assert_eq!(err.to_string(), "Authentication error: invalid credentials");
}
#[test]
fn test_share_error_display() {
let err = ShareError::InvalidToken("abc123".to_string());
assert_eq!(err.to_string(), "Invalid token: abc123");
}
#[test]
fn test_calendar_error_display() {
let err = CalendarError::InvalidColor("not-a-color".to_string());
assert_eq!(err.to_string(), "Invalid color code: not-a-color");
}
#[test]
fn test_calendar_event_error_display() {
let err = CalendarEventError::InvalidDates("end before start".to_string());
assert_eq!(err.to_string(), "Invalid event dates: end before start");
}
#[test]
fn test_errors_implement_error_trait() {
fn assert_error<E: Error>() {}
assert_error::<FileError>();
assert_error::<FolderError>();
assert_error::<UserError>();
assert_error::<ShareError>();
assert_error::<CalendarError>();
assert_error::<CalendarEventError>();
}
}
//! Pure domain entity errors
//!
//! This module defines domain entity-specific errors
//! without external framework dependencies, following
//! Clean Architecture principles.
//!
//! Errors manually implement `std::error::Error` and `std::fmt::Display`
//! to keep the domain free of external dependencies.
use std::error::Error;
use std::fmt::{Display, Formatter, Result as FmtResult};
// ============================================================================
// FILE ERRORS
// ============================================================================
/// Errors that can occur during File entity operations
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileError {
/// Occurs when the file name contains invalid characters or is empty
InvalidFileName(String),
/// Occurs when validation of any entity attribute fails
ValidationError(String),
}
impl Display for FileError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
FileError::InvalidFileName(name) => write!(f, "Invalid file name: {}", name),
FileError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
}
}
}
impl Error for FileError {}
/// Type alias for File entity operation results
pub type FileResult<T> = Result<T, FileError>;
// ============================================================================
// FOLDER ERRORS
// ============================================================================
/// Errors that can occur during Folder entity operations
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FolderError {
/// Occurs when the folder name contains invalid characters or is empty
InvalidFolderName(String),
/// Occurs when validation of any entity attribute fails
ValidationError(String),
}
impl Display for FolderError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
FolderError::InvalidFolderName(name) => write!(f, "Invalid folder name: {}", name),
FolderError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
}
}
}
impl Error for FolderError {}
/// Type alias for Folder entity operation results
pub type FolderResult<T> = Result<T, FolderError>;
// ============================================================================
// USER ERRORS
// ============================================================================
/// Errors that can occur during User entity operations
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UserError {
/// Invalid username
InvalidUsername(String),
/// Invalid password
InvalidPassword(String),
/// General validation error
ValidationError(String),
/// Authentication error
AuthenticationError(String),
}
impl Display for UserError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
UserError::InvalidUsername(msg) => write!(f, "Invalid username: {}", msg),
UserError::InvalidPassword(msg) => write!(f, "Invalid password: {}", msg),
UserError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
UserError::AuthenticationError(msg) => write!(f, "Authentication error: {}", msg),
}
}
}
impl Error for UserError {}
/// Type alias for User entity operation results
pub type UserResult<T> = Result<T, UserError>;
// ============================================================================
// SHARE ERRORS
// ============================================================================
/// Errors that can occur during Share entity operations
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ShareError {
/// Invalid share token
InvalidToken(String),
/// Invalid expiration date
InvalidExpiration(String),
/// General validation error
ValidationError(String),
}
impl Display for ShareError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
ShareError::InvalidToken(msg) => write!(f, "Invalid token: {}", msg),
ShareError::InvalidExpiration(msg) => write!(f, "Invalid expiration date: {}", msg),
ShareError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
}
}
}
impl Error for ShareError {}
/// Type alias for Share entity operation results
pub type ShareResult<T> = Result<T, ShareError>;
// ============================================================================
// CALENDAR ERRORS
// ============================================================================
/// Errors that can occur during Calendar entity operations
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CalendarError {
/// Invalid calendar name
InvalidName(String),
/// Invalid color code
InvalidColor(String),
/// Invalid owner ID
InvalidOwnerId(String),
}
impl Display for CalendarError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
CalendarError::InvalidName(msg) => write!(f, "Invalid calendar name: {}", msg),
CalendarError::InvalidColor(msg) => write!(f, "Invalid color code: {}", msg),
CalendarError::InvalidOwnerId(msg) => write!(f, "Invalid owner ID: {}", msg),
}
}
}
impl Error for CalendarError {}
/// Type alias for Calendar entity operation results
pub type CalendarResult<T> = Result<T, CalendarError>;
// ============================================================================
// CALENDAR EVENT ERRORS
// ============================================================================
/// Errors that can occur during CalendarEvent entity operations
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CalendarEventError {
/// Invalid event summary/title
InvalidSummary(String),
/// Invalid event dates
InvalidDates(String),
/// Invalid recurrence rule
InvalidRecurrence(String),
/// Invalid iCalendar data
InvalidICalData(String),
}
impl Display for CalendarEventError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
CalendarEventError::InvalidSummary(msg) => write!(f, "Invalid event summary: {}", msg),
CalendarEventError::InvalidDates(msg) => write!(f, "Invalid event dates: {}", msg),
CalendarEventError::InvalidRecurrence(msg) => {
write!(f, "Invalid recurrence rule: {}", msg)
}
CalendarEventError::InvalidICalData(msg) => {
write!(f, "Invalid iCalendar data: {}", msg)
}
}
}
}
impl Error for CalendarEventError {}
/// Type alias for CalendarEvent entity operation results
pub type CalendarEventResult<T> = Result<T, CalendarEventError>;
// ============================================================================
// TESTS
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_error_display() {
let err = FileError::InvalidFileName("test.txt".to_string());
assert_eq!(err.to_string(), "Invalid file name: test.txt");
let err = FileError::ValidationError("size too large".to_string());
assert_eq!(err.to_string(), "Validation error: size too large");
}
#[test]
fn test_folder_error_display() {
let err = FolderError::InvalidFolderName("my/folder".to_string());
assert_eq!(err.to_string(), "Invalid folder name: my/folder");
}
#[test]
fn test_user_error_display() {
let err = UserError::InvalidUsername("".to_string());
assert_eq!(err.to_string(), "Invalid username: ");
let err = UserError::AuthenticationError("invalid credentials".to_string());
assert_eq!(err.to_string(), "Authentication error: invalid credentials");
}
#[test]
fn test_share_error_display() {
let err = ShareError::InvalidToken("abc123".to_string());
assert_eq!(err.to_string(), "Invalid token: abc123");
}
#[test]
fn test_calendar_error_display() {
let err = CalendarError::InvalidColor("not-a-color".to_string());
assert_eq!(err.to_string(), "Invalid color code: not-a-color");
}
#[test]
fn test_calendar_event_error_display() {
let err = CalendarEventError::InvalidDates("end before start".to_string());
assert_eq!(err.to_string(), "Invalid event dates: end before start");
}
#[test]
fn test_errors_implement_error_trait() {
fn assert_error<E: Error>() {}
assert_error::<FileError>();
assert_error::<FolderError>();
assert_error::<UserError>();
assert_error::<ShareError>();
assert_error::<CalendarError>();
assert_error::<CalendarEventError>();
}
}
+57 -52
View File
@@ -5,11 +5,11 @@ pub use super::entity_errors::{FileError, FileResult};
/**
* Represents a file in the system's domain model.
*
*
* The File entity is a core domain object that encapsulates all properties and behaviors
* of a file in the system. It implements an immutable design pattern where modification
* operations return new instances rather than modifying the existing one.
*
*
* This entity maintains both physical storage information and logical metadata about files,
* serving as the bridge between the storage system and the application.
*/
@@ -17,28 +17,28 @@ pub use super::entity_errors::{FileError, FileResult};
pub struct File {
/// Unique identifier for the file - used throughout the system for file operations
id: String,
/// Name of the file including extension
name: String,
/// Path to the file in the domain model
storage_path: StoragePath,
/// String representation of the path for API compatibility
path_string: String,
/// Size of the file in bytes
size: u64,
/// MIME type of the file (e.g., "text/plain", "image/jpeg")
mime_type: String,
/// Parent folder ID if the file is within a folder, None if in root
folder_id: Option<String>,
/// Creation timestamp (seconds since UNIX epoch)
created_at: u64,
/// Last modification timestamp (seconds since UNIX epoch)
modified_at: u64,
}
@@ -75,15 +75,15 @@ impl File {
if name.is_empty() || name.contains('/') || name.contains('\\') {
return Err(FileError::InvalidFileName(name));
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
// Store the path string for serialization compatibility
let path_string = storage_path.to_string();
Ok(Self {
id,
name,
@@ -96,7 +96,7 @@ impl File {
modified_at: now,
})
}
/// Creates a folder entity
pub fn new_folder(
id: String,
@@ -110,23 +110,23 @@ impl File {
if name.is_empty() || name.contains('/') || name.contains('\\') {
return Err(FileError::InvalidFileName(name));
}
// Store the path string for serialization compatibility
let path_string = storage_path.to_string();
Ok(Self {
id,
name,
storage_path,
path_string,
size: 0, // Folders have zero size
size: 0, // Folders have zero size
mime_type: "directory".to_string(), // Standard MIME type for directories
folder_id: parent_id,
created_at,
modified_at,
})
}
/// Creates a file with specific timestamps (for reconstruction)
pub fn with_timestamps(
id: String,
@@ -142,10 +142,10 @@ impl File {
if name.is_empty() || name.contains('/') || name.contains('\\') {
return Err(FileError::InvalidFileName(name));
}
// Store the path string for serialization compatibility
let path_string = storage_path.to_string();
Ok(Self {
id,
name,
@@ -158,44 +158,44 @@ impl File {
modified_at,
})
}
// Getters
pub fn id(&self) -> &str {
&self.id
}
pub fn name(&self) -> &str {
&self.name
}
pub fn storage_path(&self) -> &StoragePath {
&self.storage_path
}
pub fn path_string(&self) -> &str {
&self.path_string
}
pub fn size(&self) -> u64 {
self.size
}
pub fn mime_type(&self) -> &str {
&self.mime_type
}
pub fn folder_id(&self) -> Option<&str> {
self.folder_id.as_deref()
}
pub fn created_at(&self) -> u64 {
self.created_at
}
pub fn modified_at(&self) -> u64 {
self.modified_at
}
/// Creates a new File instance from a DTO
/// This function is primarily for conversions in batch handlers
pub fn from_dto(
@@ -210,7 +210,7 @@ impl File {
) -> Self {
// Create storage_path from string
let storage_path = StoragePath::from_string(&path);
// Create directly without validation to avoid errors in DTO conversions
Self {
id,
@@ -224,31 +224,31 @@ impl File {
modified_at,
}
}
// Methods to create new versions of the file (immutable)
/// Creates a new version of the file with updated name
pub fn with_name(&self, new_name: String) -> FileResult<Self> {
// Validate file name
if new_name.is_empty() || new_name.contains('/') || new_name.contains('\\') {
return Err(FileError::InvalidFileName(new_name));
}
// Update path based on name
let parent_path = self.storage_path.parent();
let new_storage_path = match parent_path {
Some(parent) => parent.join(&new_name),
None => StoragePath::from_string(&new_name),
};
// Update string representation
let new_path_string = new_storage_path.to_string();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Ok(Self {
id: self.id.clone(),
name: new_name,
@@ -261,23 +261,27 @@ impl File {
modified_at: now,
})
}
/// Creates a new version of the file with updated folder
pub fn with_folder(&self, folder_id: Option<String>, folder_path: Option<StoragePath>) -> FileResult<Self> {
pub fn with_folder(
&self,
folder_id: Option<String>,
folder_path: Option<StoragePath>,
) -> FileResult<Self> {
// We need a folder path to update the file path
let new_storage_path = match folder_path {
Some(path) => path.join(&self.name),
None => StoragePath::from_string(&self.name), // Root
};
// Update string representation
let new_path_string = new_storage_path.to_string();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Ok(Self {
id: self.id.clone(),
name: self.name.clone(),
@@ -290,14 +294,14 @@ impl File {
modified_at: now,
})
}
/// Creates a new version of the file with updated size
pub fn with_size(&self, new_size: u64) -> Self {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Self {
id: self.id.clone(),
name: self.name.clone(),
@@ -315,7 +319,7 @@ impl File {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_creation_with_valid_name() {
let storage_path = StoragePath::from_string("/test/file.txt");
@@ -327,10 +331,10 @@ mod tests {
"text/plain".to_string(),
None,
);
assert!(file.is_ok());
}
#[test]
fn test_file_creation_with_invalid_name() {
let storage_path = StoragePath::from_string("/test/invalid/file.txt");
@@ -342,14 +346,14 @@ mod tests {
"text/plain".to_string(),
None,
);
assert!(file.is_err());
match file {
Err(FileError::InvalidFileName(_)) => (),
_ => panic!("Expected InvalidFileName error"),
}
}
#[test]
fn test_file_with_name() {
let storage_path = StoragePath::from_string("/test/file.txt");
@@ -360,12 +364,13 @@ mod tests {
100,
"text/plain".to_string(),
None,
).unwrap();
)
.unwrap();
let renamed = file.with_name("newname.txt".to_string());
assert!(renamed.is_ok());
let renamed = renamed.unwrap();
assert_eq!(renamed.name(), "newname.txt");
assert_eq!(renamed.id(), "123"); // The ID does not change
}
}
}
+49 -44
View File
@@ -8,22 +8,22 @@ pub use super::entity_errors::{FolderError, FolderResult};
pub struct Folder {
/// Unique identifier for the folder
id: String,
/// Name of the folder
name: String,
/// Path to the folder in the domain model
storage_path: StoragePath,
/// String representation of the path (for API compatibility)
path_string: String,
/// Parent folder ID (None if it's a root folder)
parent_id: Option<String>,
/// Creation timestamp
created_at: u64,
/// Last modification timestamp
modified_at: u64,
}
@@ -56,15 +56,15 @@ impl Folder {
if name.is_empty() || name.contains('/') || name.contains('\\') {
return Err(FolderError::InvalidFolderName(name));
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
// Store the path string for serialization compatibility
let path_string = storage_path.to_string();
Ok(Self {
id,
name,
@@ -75,7 +75,7 @@ impl Folder {
modified_at: now,
})
}
/// Creates a folder with specific timestamps (for reconstruction)
pub fn with_timestamps(
id: String,
@@ -89,10 +89,10 @@ impl Folder {
if name.is_empty() || name.contains('/') || name.contains('\\') {
return Err(FolderError::InvalidFolderName(name));
}
// Store the path string for serialization compatibility
let path_string = storage_path.to_string();
Ok(Self {
id,
name,
@@ -103,36 +103,36 @@ impl Folder {
modified_at,
})
}
// Getters
pub fn id(&self) -> &str {
&self.id
}
pub fn name(&self) -> &str {
&self.name
}
pub fn storage_path(&self) -> &StoragePath {
&self.storage_path
}
pub fn path_string(&self) -> &str {
&self.path_string
}
pub fn parent_id(&self) -> Option<&str> {
self.parent_id.as_deref()
}
pub fn created_at(&self) -> u64 {
self.created_at
}
pub fn modified_at(&self) -> u64 {
self.modified_at
}
/// Creates a new Folder instance from a DTO
/// This function is primarily for conversions in batch handlers
pub fn from_dto(
@@ -145,7 +145,7 @@ impl Folder {
) -> Self {
// Create storage_path from the string
let storage_path = StoragePath::from_string(&path);
// Create directly without validation to avoid errors in DTO conversions
Self {
id,
@@ -157,31 +157,31 @@ impl Folder {
modified_at,
}
}
// Methods to create new versions of the folder (immutable)
/// Creates a new version of the folder with updated name
pub fn with_name(&self, new_name: String) -> FolderResult<Self> {
// Validate folder name
if new_name.is_empty() || new_name.contains('/') || new_name.contains('\\') {
return Err(FolderError::InvalidFolderName(new_name));
}
// Update path based on the name
let parent_path = self.storage_path.parent();
let new_storage_path = match parent_path {
Some(parent) => parent.join(&new_name),
None => StoragePath::from_string(&new_name),
};
// Update string representation
let new_path_string = new_storage_path.to_string();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Ok(Self {
id: self.id.clone(),
name: new_name,
@@ -192,23 +192,27 @@ impl Folder {
modified_at: now,
})
}
/// Creates a new version of the folder with updated parent
pub fn with_parent(&self, parent_id: Option<String>, parent_path: Option<StoragePath>) -> FolderResult<Self> {
pub fn with_parent(
&self,
parent_id: Option<String>,
parent_path: Option<StoragePath>,
) -> FolderResult<Self> {
// We need a folder path to update the path
let new_storage_path = match parent_path {
Some(path) => path.join(&self.name),
None => StoragePath::from_string(&self.name), // Root
};
// Update string representation
let new_path_string = new_storage_path.to_string();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Ok(Self {
id: self.id.clone(),
name: self.name.clone(),
@@ -219,22 +223,22 @@ impl Folder {
modified_at: now,
})
}
/// Returns an absolute path for this folder
pub fn get_absolute_path<P: AsRef<std::path::Path>>(&self, root_path: P) -> std::path::PathBuf {
let mut result = std::path::PathBuf::from(root_path.as_ref());
// Skip leading '/' from path_string to avoid creating absolute path incorrectly
let relative_path = if self.path_string.starts_with('/') {
&self.path_string[1..]
} else {
&self.path_string
};
if !relative_path.is_empty() {
result.push(relative_path);
}
result
}
}
@@ -242,7 +246,7 @@ impl Folder {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_folder_creation_with_valid_name() {
let storage_path = StoragePath::from_string("/test/folder");
@@ -252,10 +256,10 @@ mod tests {
storage_path,
None,
);
assert!(folder.is_ok());
}
#[test]
fn test_folder_creation_with_invalid_name() {
let storage_path = StoragePath::from_string("/test/invalid/folder");
@@ -265,14 +269,14 @@ mod tests {
storage_path,
None,
);
assert!(folder.is_err());
match folder {
Err(FolderError::InvalidFolderName(_)) => (),
_ => panic!("Expected InvalidFolderName error"),
}
}
#[test]
fn test_folder_with_name() {
let storage_path = StoragePath::from_string("/test/folder");
@@ -281,12 +285,13 @@ mod tests {
"old_name".to_string(),
storage_path,
None,
).unwrap();
)
.unwrap();
let renamed = folder.with_name("new_name".to_string());
assert!(renamed.is_ok());
let renamed = renamed.unwrap();
assert_eq!(renamed.name(), "new_name");
assert_eq!(renamed.id(), "123"); // The ID doesn't change
}
}
}
+4 -8
View File
@@ -4,17 +4,13 @@ pub mod contact;
pub mod entity_errors;
pub mod file;
pub mod folder;
pub mod user;
pub mod session;
pub mod share;
pub mod trashed_item;
pub mod user;
// Re-exportar errores de entidades para facilitar el uso
pub use entity_errors::{
FileError, FileResult,
FolderError, FolderResult,
UserError, UserResult,
ShareError, ShareResult,
CalendarError, CalendarResult,
CalendarEventError, CalendarEventResult,
};
CalendarError, CalendarEventError, CalendarEventResult, CalendarResult, FileError, FileResult,
FolderError, FolderResult, ShareError, ShareResult, UserError, UserResult,
};
+10 -10
View File
@@ -1,5 +1,5 @@
use chrono::{DateTime, Duration, Utc};
use uuid::Uuid;
use chrono::{DateTime, Utc, Duration};
#[derive(Debug, Clone)]
pub struct Session {
@@ -64,20 +64,20 @@ impl Session {
revoked,
}
}
// Getters
pub fn id(&self) -> &str {
&self.id
}
pub fn user_id(&self) -> &str {
&self.user_id
}
pub fn refresh_token(&self) -> &str {
&self.refresh_token
}
pub fn expires_at(&self) -> DateTime<Utc> {
self.expires_at
}
@@ -89,20 +89,20 @@ impl Session {
pub fn user_agent(&self) -> Option<&str> {
self.user_agent.as_deref()
}
pub fn created_at(&self) -> DateTime<Utc> {
self.created_at
}
pub fn is_expired(&self) -> bool {
Utc::now() > self.expires_at
}
pub fn is_revoked(&self) -> bool {
self.revoked
}
pub fn revoke(&mut self) {
self.revoked = true;
}
}
}
+37 -21
View File
@@ -33,7 +33,7 @@ pub enum ShareItemType {
impl Share {
pub fn new(
item_id: String,
item_id: String,
item_type: ShareItemType,
created_by: String,
permissions: Option<SharePermissions>,
@@ -42,7 +42,9 @@ impl Share {
) -> Result<Self, ShareError> {
// Validate item_id
if item_id.is_empty() {
return Err(ShareError::ValidationError("Item ID cannot be empty".to_string()));
return Err(ShareError::ValidationError(
"Item ID cannot be empty".to_string(),
));
}
// Validate expiration date if provided
@@ -51,9 +53,11 @@ impl Share {
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs();
if expires <= now {
return Err(ShareError::InvalidExpiration("Expiration date must be in the future".to_string()));
return Err(ShareError::InvalidExpiration(
"Expiration date must be in the future".to_string(),
));
}
}
@@ -174,10 +178,10 @@ impl Share {
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs();
return expires_at <= now;
}
false
}
@@ -192,7 +196,7 @@ impl Share {
}
/// Returns a reference to the password hash, if one is set.
///
///
/// Password verification should be performed externally via PasswordHasherPort
/// to keep cryptographic dependencies out of the domain layer.
pub fn password_hash(&self) -> Option<&str> {
@@ -238,7 +242,10 @@ impl TryFrom<&str> for ShareItemType {
match s.to_lowercase().as_str() {
"file" => Ok(ShareItemType::File),
"folder" => Ok(ShareItemType::Folder),
_ => Err(ShareError::ValidationError(format!("Invalid item type: {}", s))),
_ => Err(ShareError::ValidationError(format!(
"Invalid item type: {}",
s
))),
}
}
}
@@ -276,7 +283,7 @@ mod tests {
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs();
// Create a share that expires in the future
let future = now + 3600; // 1 hour in the future
let share = Share::new(
@@ -288,9 +295,9 @@ mod tests {
Some(future),
)
.unwrap();
assert!(!share.is_expired());
// Test with past expiration (should fail during creation)
let past = now - 3600; // 1 hour in the past
let share_result = Share::new(
@@ -301,21 +308,30 @@ mod tests {
None,
Some(past),
);
assert!(share_result.is_err());
}
#[test]
fn test_share_item_type_conversion() {
assert_eq!(ShareItemType::File.to_string(), "file");
assert_eq!(ShareItemType::Folder.to_string(), "folder");
assert_eq!(ShareItemType::try_from("file").unwrap(), ShareItemType::File);
assert_eq!(ShareItemType::try_from("folder").unwrap(), ShareItemType::Folder);
assert_eq!(ShareItemType::try_from("FILE").unwrap(), ShareItemType::File);
assert_eq!(
ShareItemType::try_from("file").unwrap(),
ShareItemType::File
);
assert_eq!(
ShareItemType::try_from("folder").unwrap(),
ShareItemType::Folder
);
assert_eq!(
ShareItemType::try_from("FILE").unwrap(),
ShareItemType::File
);
assert!(ShareItemType::try_from("invalid").is_err());
}
#[test]
fn test_has_password_with_hash() {
let share = Share::new(
@@ -327,11 +343,11 @@ mod tests {
None,
)
.unwrap();
assert!(share.has_password());
assert_eq!(share.password_hash(), Some("some_hash_value"));
}
#[test]
fn test_has_password_without_hash() {
let share = Share::new(
@@ -343,7 +359,7 @@ mod tests {
None,
)
.unwrap();
assert!(!share.has_password());
assert_eq!(share.password_hash(), None);
}
+1 -1
View File
@@ -103,4 +103,4 @@ impl TrashedItem {
let now = Utc::now();
(self.deletion_date - now).num_days().max(0)
}
}
}
+36 -34
View File
@@ -1,5 +1,5 @@
use uuid::Uuid;
use chrono::{DateTime, Utc};
use uuid::Uuid;
// Re-export entity errors from the centralized module
pub use super::entity_errors::{UserError, UserResult};
@@ -23,7 +23,7 @@ impl std::fmt::Display for UserRole {
#[derive(Debug, Clone)]
pub struct User {
id: String,
username: String,
username: String,
email: String,
password_hash: String,
role: UserRole,
@@ -39,11 +39,11 @@ pub struct User {
impl User {
/// Create a new user with a pre-hashed password.
///
///
/// The password hashing should be done externally using PasswordHasherPort
/// to maintain clean architecture and keep cryptographic dependencies
/// out of the domain layer.
///
///
/// # Arguments
/// * `username` - User's username (3-32 characters)
/// * `email` - User's email address
@@ -52,26 +52,30 @@ impl User {
/// * `storage_quota_bytes` - Storage quota in bytes
pub fn new(
username: String,
email: String,
email: String,
password_hash: String,
role: UserRole,
storage_quota_bytes: i64,
) -> UserResult<Self> {
// Validations
if username.is_empty() || username.len() < 3 || username.len() > 32 {
return Err(UserError::InvalidUsername("Username must be between 3 and 32 characters".to_string()));
return Err(UserError::InvalidUsername(
"Username must be between 3 and 32 characters".to_string(),
));
}
if !email.contains('@') || email.len() < 5 {
return Err(UserError::ValidationError("Invalid email".to_string()));
}
if password_hash.is_empty() {
return Err(UserError::InvalidPassword("Password hash cannot be empty".to_string()));
return Err(UserError::InvalidPassword(
"Password hash cannot be empty".to_string(),
));
}
let now = Utc::now();
Ok(Self {
id: Uuid::new_v4().to_string(),
username,
@@ -88,7 +92,7 @@ impl User {
oidc_subject: None,
})
}
/// Create a new OIDC-authenticated user (no password required).
pub fn new_oidc(
username: String,
@@ -104,9 +108,7 @@ impl User {
));
}
if !email.contains('@') || email.len() < 5 {
return Err(UserError::ValidationError(
"Invalid email".to_string(),
));
return Err(UserError::ValidationError("Invalid email".to_string()));
}
let now = Utc::now();
Ok(Self {
@@ -125,7 +127,7 @@ impl User {
oidc_subject: Some(oidc_subject),
})
}
// Create from existing values (for reconstruction from DB)
pub fn from_data(
id: String,
@@ -189,48 +191,48 @@ impl User {
oidc_subject,
}
}
// Getters
pub fn id(&self) -> &str {
&self.id
}
pub fn username(&self) -> &str {
&self.username
}
pub fn email(&self) -> &str {
&self.email
}
pub fn role(&self) -> UserRole {
self.role
}
pub fn storage_quota_bytes(&self) -> i64 {
self.storage_quota_bytes
}
pub fn storage_used_bytes(&self) -> i64 {
self.storage_used_bytes
}
pub fn created_at(&self) -> DateTime<Utc> {
self.created_at
}
pub fn updated_at(&self) -> DateTime<Utc> {
self.updated_at
}
pub fn last_login_at(&self) -> Option<DateTime<Utc>> {
self.last_login_at
}
pub fn is_active(&self) -> bool {
self.active
}
pub fn password_hash(&self) -> &str {
&self.password_hash
}
@@ -247,38 +249,38 @@ impl User {
pub fn is_oidc_user(&self) -> bool {
self.oidc_provider.is_some()
}
/// Update the password hash.
///
///
/// The new password should be hashed externally using PasswordHasherPort
/// before calling this method.
pub fn update_password_hash(&mut self, new_hash: String) {
self.password_hash = new_hash;
self.updated_at = Utc::now();
}
// Update storage usage
pub fn update_storage_used(&mut self, storage_used_bytes: i64) {
self.storage_used_bytes = storage_used_bytes;
self.updated_at = Utc::now();
}
// Register login
pub fn register_login(&mut self) {
let now = Utc::now();
self.last_login_at = Some(now);
self.updated_at = now;
}
// Deactivate user
pub fn deactivate(&mut self) {
self.active = false;
self.updated_at = Utc::now();
}
// Activate user
pub fn activate(&mut self) {
self.active = true;
self.updated_at = Utc::now();
}
}
}