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();
}
}
}
+267 -271
View File
@@ -1,271 +1,267 @@
//! Domain errors
//!
//! This module contains domain-specific error types.
//! DomainError is the base error used throughout the domain layer.
use std::fmt::{Display, Formatter, Result as FmtResult};
use std::error::Error as StdError;
use thiserror::Error;
/// Common Result type for the domain with DomainError as the standard error
pub type Result<T> = std::result::Result<T, DomainError>;
/// Domain error types
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorKind {
/// Entity not found
NotFound,
/// Entity already exists
AlreadyExists,
/// Invalid input or failed validation
InvalidInput,
/// Access or permissions error
AccessDenied,
/// Timeout expired
Timeout,
/// Internal system error
InternalError,
/// Functionality not implemented
NotImplemented,
/// Unsupported operation
UnsupportedOperation,
/// Database error
DatabaseError,
}
impl Display for ErrorKind {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
ErrorKind::NotFound => write!(f, "Not Found"),
ErrorKind::AlreadyExists => write!(f, "Already Exists"),
ErrorKind::InvalidInput => write!(f, "Invalid Input"),
ErrorKind::AccessDenied => write!(f, "Access Denied"),
ErrorKind::Timeout => write!(f, "Timeout"),
ErrorKind::InternalError => write!(f, "Internal Error"),
ErrorKind::NotImplemented => write!(f, "Not Implemented"),
ErrorKind::UnsupportedOperation => write!(f, "Unsupported Operation"),
ErrorKind::DatabaseError => write!(f, "Database Error"),
}
}
}
/// Base domain error that provides detailed context
#[derive(Error, Debug)]
#[error("{kind}: {message}")]
pub struct DomainError {
/// Error type
pub kind: ErrorKind,
/// Affected entity type (e.g.: "File", "Folder")
pub entity_type: &'static str,
/// Entity identifier if available
pub entity_id: Option<String>,
/// Descriptive error message
pub message: String,
/// Source error (optional)
#[source]
pub source: Option<Box<dyn StdError + Send + Sync>>,
}
impl DomainError {
/// Creates a new domain error
pub fn new<S: Into<String>>(
kind: ErrorKind,
entity_type: &'static str,
message: S,
) -> Self {
Self {
kind,
entity_type,
entity_id: None,
message: message.into(),
source: None,
}
}
/// Creates an entity not found error
pub fn not_found<S: Into<String>>(entity_type: &'static str, entity_id: S) -> Self {
let id = entity_id.into();
Self {
kind: ErrorKind::NotFound,
entity_type,
entity_id: Some(id.clone()),
message: format!("{} not found: {}", entity_type, id),
source: None,
}
}
/// Creates an entity already exists error
pub fn already_exists<S: Into<String>>(entity_type: &'static str, entity_id: S) -> Self {
let id = entity_id.into();
Self {
kind: ErrorKind::AlreadyExists,
entity_type,
entity_id: Some(id.clone()),
message: format!("{} already exists: {}", entity_type, id),
source: None,
}
}
/// Creates an error for unsupported operations
pub fn operation_not_supported<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
Self::new(
ErrorKind::UnsupportedOperation,
entity_type,
message,
)
}
/// Creates a timeout error
pub fn timeout<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
Self {
kind: ErrorKind::Timeout,
entity_type,
entity_id: None,
message: message.into(),
source: None,
}
}
/// Creates an internal error
pub fn internal_error<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
Self {
kind: ErrorKind::InternalError,
entity_type,
entity_id: None,
message: message.into(),
source: None,
}
}
/// Creates an access denied error
pub fn access_denied<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
Self {
kind: ErrorKind::AccessDenied,
entity_type,
entity_id: None,
message: message.into(),
source: None,
}
}
/// Alias for access_denied to maintain compatibility
pub fn unauthorized<S: Into<String>>(message: S) -> Self {
Self {
kind: ErrorKind::AccessDenied,
entity_type: "Authorization",
entity_id: None,
message: message.into(),
source: None,
}
}
/// Creates a database error
pub fn database_error<S: Into<String>>(message: S) -> Self {
Self {
kind: ErrorKind::DatabaseError,
entity_type: "Database",
entity_id: None,
message: message.into(),
source: None,
}
}
/// Creates a validation error
pub fn validation_error<S: Into<String>>(message: S) -> Self {
Self {
kind: ErrorKind::InvalidInput,
entity_type: "Validation",
entity_id: None,
message: message.into(),
source: None,
}
}
/// Creates a not implemented error
pub fn not_implemented<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
Self {
kind: ErrorKind::NotImplemented,
entity_type,
entity_id: None,
message: message.into(),
source: None,
}
}
/// Sets the entity ID
pub fn with_id<S: Into<String>>(mut self, entity_id: S) -> Self {
self.entity_id = Some(entity_id.into());
self
}
/// Sets the source error
pub fn with_source<E: StdError + Send + Sync + 'static>(mut self, source: E) -> Self {
self.source = Some(Box::new(source));
self
}
}
/// Trait for adding context to errors
pub trait ErrorContext<T, E> {
fn with_context<C, F>(self, context: F) -> std::result::Result<T, DomainError>
where
C: Into<String>,
F: FnOnce() -> C;
fn with_error_kind(self, kind: ErrorKind, entity_type: &'static str) -> std::result::Result<T, DomainError>;
}
impl<T, E: StdError + Send + Sync + 'static> ErrorContext<T, E> for std::result::Result<T, E> {
fn with_context<C, F>(self, context: F) -> std::result::Result<T, DomainError>
where
C: Into<String>,
F: FnOnce() -> C,
{
self.map_err(|e| {
DomainError {
kind: ErrorKind::InternalError,
entity_type: "Unknown",
entity_id: None,
message: context().into(),
source: Some(Box::new(e)),
}
})
}
fn with_error_kind(self, kind: ErrorKind, entity_type: &'static str) -> std::result::Result<T, DomainError> {
self.map_err(|e| {
DomainError {
kind,
entity_type,
entity_id: None,
message: format!("{}", e),
source: Some(Box::new(e)),
}
})
}
}
// From implementations for standard errors (without external infrastructure dependencies)
impl From<std::io::Error> for DomainError {
fn from(err: std::io::Error) -> Self {
DomainError {
kind: ErrorKind::InternalError,
entity_type: "IO",
entity_id: None,
message: format!("{}", err),
source: Some(Box::new(err)),
}
}
}
impl From<uuid::Error> for DomainError {
fn from(err: uuid::Error) -> Self {
DomainError {
kind: ErrorKind::InvalidInput,
entity_type: "UUID",
entity_id: None,
message: format!("{}", err),
source: Some(Box::new(err)),
}
}
}
//! Domain errors
//!
//! This module contains domain-specific error types.
//! DomainError is the base error used throughout the domain layer.
use std::error::Error as StdError;
use std::fmt::{Display, Formatter, Result as FmtResult};
use thiserror::Error;
/// Common Result type for the domain with DomainError as the standard error
pub type Result<T> = std::result::Result<T, DomainError>;
/// Domain error types
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorKind {
/// Entity not found
NotFound,
/// Entity already exists
AlreadyExists,
/// Invalid input or failed validation
InvalidInput,
/// Access or permissions error
AccessDenied,
/// Timeout expired
Timeout,
/// Internal system error
InternalError,
/// Functionality not implemented
NotImplemented,
/// Unsupported operation
UnsupportedOperation,
/// Database error
DatabaseError,
}
impl Display for ErrorKind {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
ErrorKind::NotFound => write!(f, "Not Found"),
ErrorKind::AlreadyExists => write!(f, "Already Exists"),
ErrorKind::InvalidInput => write!(f, "Invalid Input"),
ErrorKind::AccessDenied => write!(f, "Access Denied"),
ErrorKind::Timeout => write!(f, "Timeout"),
ErrorKind::InternalError => write!(f, "Internal Error"),
ErrorKind::NotImplemented => write!(f, "Not Implemented"),
ErrorKind::UnsupportedOperation => write!(f, "Unsupported Operation"),
ErrorKind::DatabaseError => write!(f, "Database Error"),
}
}
}
/// Base domain error that provides detailed context
#[derive(Error, Debug)]
#[error("{kind}: {message}")]
pub struct DomainError {
/// Error type
pub kind: ErrorKind,
/// Affected entity type (e.g.: "File", "Folder")
pub entity_type: &'static str,
/// Entity identifier if available
pub entity_id: Option<String>,
/// Descriptive error message
pub message: String,
/// Source error (optional)
#[source]
pub source: Option<Box<dyn StdError + Send + Sync>>,
}
impl DomainError {
/// Creates a new domain error
pub fn new<S: Into<String>>(kind: ErrorKind, entity_type: &'static str, message: S) -> Self {
Self {
kind,
entity_type,
entity_id: None,
message: message.into(),
source: None,
}
}
/// Creates an entity not found error
pub fn not_found<S: Into<String>>(entity_type: &'static str, entity_id: S) -> Self {
let id = entity_id.into();
Self {
kind: ErrorKind::NotFound,
entity_type,
entity_id: Some(id.clone()),
message: format!("{} not found: {}", entity_type, id),
source: None,
}
}
/// Creates an entity already exists error
pub fn already_exists<S: Into<String>>(entity_type: &'static str, entity_id: S) -> Self {
let id = entity_id.into();
Self {
kind: ErrorKind::AlreadyExists,
entity_type,
entity_id: Some(id.clone()),
message: format!("{} already exists: {}", entity_type, id),
source: None,
}
}
/// Creates an error for unsupported operations
pub fn operation_not_supported<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
Self::new(ErrorKind::UnsupportedOperation, entity_type, message)
}
/// Creates a timeout error
pub fn timeout<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
Self {
kind: ErrorKind::Timeout,
entity_type,
entity_id: None,
message: message.into(),
source: None,
}
}
/// Creates an internal error
pub fn internal_error<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
Self {
kind: ErrorKind::InternalError,
entity_type,
entity_id: None,
message: message.into(),
source: None,
}
}
/// Creates an access denied error
pub fn access_denied<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
Self {
kind: ErrorKind::AccessDenied,
entity_type,
entity_id: None,
message: message.into(),
source: None,
}
}
/// Alias for access_denied to maintain compatibility
pub fn unauthorized<S: Into<String>>(message: S) -> Self {
Self {
kind: ErrorKind::AccessDenied,
entity_type: "Authorization",
entity_id: None,
message: message.into(),
source: None,
}
}
/// Creates a database error
pub fn database_error<S: Into<String>>(message: S) -> Self {
Self {
kind: ErrorKind::DatabaseError,
entity_type: "Database",
entity_id: None,
message: message.into(),
source: None,
}
}
/// Creates a validation error
pub fn validation_error<S: Into<String>>(message: S) -> Self {
Self {
kind: ErrorKind::InvalidInput,
entity_type: "Validation",
entity_id: None,
message: message.into(),
source: None,
}
}
/// Creates a not implemented error
pub fn not_implemented<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
Self {
kind: ErrorKind::NotImplemented,
entity_type,
entity_id: None,
message: message.into(),
source: None,
}
}
/// Sets the entity ID
pub fn with_id<S: Into<String>>(mut self, entity_id: S) -> Self {
self.entity_id = Some(entity_id.into());
self
}
/// Sets the source error
pub fn with_source<E: StdError + Send + Sync + 'static>(mut self, source: E) -> Self {
self.source = Some(Box::new(source));
self
}
}
/// Trait for adding context to errors
pub trait ErrorContext<T, E> {
fn with_context<C, F>(self, context: F) -> std::result::Result<T, DomainError>
where
C: Into<String>,
F: FnOnce() -> C;
fn with_error_kind(
self,
kind: ErrorKind,
entity_type: &'static str,
) -> std::result::Result<T, DomainError>;
}
impl<T, E: StdError + Send + Sync + 'static> ErrorContext<T, E> for std::result::Result<T, E> {
fn with_context<C, F>(self, context: F) -> std::result::Result<T, DomainError>
where
C: Into<String>,
F: FnOnce() -> C,
{
self.map_err(|e| DomainError {
kind: ErrorKind::InternalError,
entity_type: "Unknown",
entity_id: None,
message: context().into(),
source: Some(Box::new(e)),
})
}
fn with_error_kind(
self,
kind: ErrorKind,
entity_type: &'static str,
) -> std::result::Result<T, DomainError> {
self.map_err(|e| DomainError {
kind,
entity_type,
entity_id: None,
message: format!("{}", e),
source: Some(Box::new(e)),
})
}
}
// From implementations for standard errors (without external infrastructure dependencies)
impl From<std::io::Error> for DomainError {
fn from(err: std::io::Error) -> Self {
DomainError {
kind: ErrorKind::InternalError,
entity_type: "IO",
entity_id: None,
message: format!("{}", err),
source: Some(Box::new(err)),
}
}
}
impl From<uuid::Error> for DomainError {
fn from(err: uuid::Error) -> Self {
DomainError {
kind: ErrorKind::InvalidInput,
entity_type: "UUID",
entity_id: None,
message: format!("{}", err),
source: Some(Box::new(err)),
}
}
}
@@ -1,6 +1,6 @@
use async_trait::async_trait;
use uuid::Uuid;
use std::result::Result;
use uuid::Uuid;
use crate::common::errors::DomainError;
use crate::domain::entities::contact::AddressBook;
@@ -9,14 +9,41 @@ pub type AddressBookRepositoryResult<T> = Result<T, DomainError>;
#[async_trait]
pub trait AddressBookRepository: Send + Sync + 'static {
async fn create_address_book(&self, address_book: AddressBook) -> AddressBookRepositoryResult<AddressBook>;
async fn update_address_book(&self, address_book: AddressBook) -> AddressBookRepositoryResult<AddressBook>;
async fn create_address_book(
&self,
address_book: AddressBook,
) -> AddressBookRepositoryResult<AddressBook>;
async fn update_address_book(
&self,
address_book: AddressBook,
) -> AddressBookRepositoryResult<AddressBook>;
async fn delete_address_book(&self, id: &Uuid) -> AddressBookRepositoryResult<()>;
async fn get_address_book_by_id(&self, id: &Uuid) -> AddressBookRepositoryResult<Option<AddressBook>>;
async fn get_address_books_by_owner(&self, owner_id: &str) -> AddressBookRepositoryResult<Vec<AddressBook>>;
async fn get_shared_address_books(&self, user_id: &str) -> AddressBookRepositoryResult<Vec<AddressBook>>;
async fn get_address_book_by_id(
&self,
id: &Uuid,
) -> AddressBookRepositoryResult<Option<AddressBook>>;
async fn get_address_books_by_owner(
&self,
owner_id: &str,
) -> AddressBookRepositoryResult<Vec<AddressBook>>;
async fn get_shared_address_books(
&self,
user_id: &str,
) -> AddressBookRepositoryResult<Vec<AddressBook>>;
async fn get_public_address_books(&self) -> AddressBookRepositoryResult<Vec<AddressBook>>;
async fn share_address_book(&self, address_book_id: &Uuid, user_id: &str, can_write: bool) -> AddressBookRepositoryResult<()>;
async fn unshare_address_book(&self, address_book_id: &Uuid, user_id: &str) -> AddressBookRepositoryResult<()>;
async fn get_address_book_shares(&self, address_book_id: &Uuid) -> AddressBookRepositoryResult<Vec<(String, bool)>>;
}
async fn share_address_book(
&self,
address_book_id: &Uuid,
user_id: &str,
can_write: bool,
) -> AddressBookRepositoryResult<()>;
async fn unshare_address_book(
&self,
address_book_id: &Uuid,
user_id: &str,
) -> AddressBookRepositoryResult<()>;
async fn get_address_book_shares(
&self,
address_book_id: &Uuid,
) -> AddressBookRepositoryResult<Vec<(String, bool)>>;
}
@@ -1,8 +1,8 @@
use async_trait::async_trait;
use uuid::Uuid;
use chrono::{DateTime, Utc};
use crate::common::errors::DomainError;
use crate::domain::entities::calendar_event::CalendarEvent;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use uuid::Uuid;
pub type CalendarEventRepositoryResult<T> = Result<T, DomainError>;
@@ -10,53 +10,76 @@ pub type CalendarEventRepositoryResult<T> = Result<T, DomainError>;
#[async_trait]
pub trait CalendarEventRepository: Send + Sync + 'static {
/// Creates a new calendar event
async fn create_event(&self, event: CalendarEvent) -> CalendarEventRepositoryResult<CalendarEvent>;
async fn create_event(
&self,
event: CalendarEvent,
) -> CalendarEventRepositoryResult<CalendarEvent>;
/// Updates an existing calendar event
async fn update_event(&self, event: CalendarEvent) -> CalendarEventRepositoryResult<CalendarEvent>;
async fn update_event(
&self,
event: CalendarEvent,
) -> CalendarEventRepositoryResult<CalendarEvent>;
/// Deletes a calendar event by ID
async fn delete_event(&self, id: &Uuid) -> CalendarEventRepositoryResult<()>;
/// Finds a calendar event by its ID
async fn find_event_by_id(&self, id: &Uuid) -> CalendarEventRepositoryResult<CalendarEvent>;
/// Lists all events in a specific calendar
async fn list_events_by_calendar(&self, calendar_id: &Uuid) -> CalendarEventRepositoryResult<Vec<CalendarEvent>>;
async fn list_events_by_calendar(
&self,
calendar_id: &Uuid,
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>>;
/// Finds events in a calendar by their summary/title (partial match)
async fn find_events_by_summary(&self, calendar_id: &Uuid, summary: &str) -> CalendarEventRepositoryResult<Vec<CalendarEvent>>;
async fn find_events_by_summary(
&self,
calendar_id: &Uuid,
summary: &str,
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>>;
/// Gets events in a specific time range for a calendar
async fn get_events_in_time_range(
&self,
calendar_id: &Uuid,
start: &DateTime<Utc>,
end: &DateTime<Utc>
&self,
calendar_id: &Uuid,
start: &DateTime<Utc>,
end: &DateTime<Utc>,
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>>;
/// Finds an event by its iCalendar UID in a specific calendar
async fn find_event_by_ical_uid(&self, calendar_id: &Uuid, ical_uid: &str) -> CalendarEventRepositoryResult<Option<CalendarEvent>>;
async fn find_event_by_ical_uid(
&self,
calendar_id: &Uuid,
ical_uid: &str,
) -> CalendarEventRepositoryResult<Option<CalendarEvent>>;
/// Counts events in a calendar
async fn count_events_in_calendar(&self, calendar_id: &Uuid) -> CalendarEventRepositoryResult<i64>;
async fn count_events_in_calendar(
&self,
calendar_id: &Uuid,
) -> CalendarEventRepositoryResult<i64>;
/// Deletes all events in a calendar
async fn delete_all_events_in_calendar(&self, calendar_id: &Uuid) -> CalendarEventRepositoryResult<i64>;
async fn delete_all_events_in_calendar(
&self,
calendar_id: &Uuid,
) -> CalendarEventRepositoryResult<i64>;
/// Lists events by calendar with pagination
async fn list_events_by_calendar_paginated(
&self,
&self,
calendar_id: &Uuid,
limit: i64,
offset: i64
offset: i64,
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>>;
/// Finds events with recurrence rules that might occur in a time range
async fn find_recurring_events_in_range(
&self,
calendar_id: &Uuid,
start: &DateTime<Utc>,
end: &DateTime<Utc>
end: &DateTime<Utc>,
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>>;
}
}
+76 -30
View File
@@ -1,7 +1,7 @@
use async_trait::async_trait;
use uuid::Uuid;
use crate::common::errors::DomainError;
use crate::domain::entities::calendar::Calendar;
use async_trait::async_trait;
use uuid::Uuid;
pub type CalendarRepositoryResult<T> = Result<T, DomainError>;
@@ -10,49 +10,95 @@ pub type CalendarRepositoryResult<T> = Result<T, DomainError>;
pub trait CalendarRepository: Send + Sync + 'static {
/// Creates a new calendar
async fn create_calendar(&self, calendar: Calendar) -> CalendarRepositoryResult<Calendar>;
/// Updates an existing calendar
async fn update_calendar(&self, calendar: Calendar) -> CalendarRepositoryResult<Calendar>;
/// Deletes a calendar by ID
async fn delete_calendar(&self, id: &Uuid) -> CalendarRepositoryResult<()>;
/// Finds a calendar by its ID
async fn find_calendar_by_id(&self, id: &Uuid) -> CalendarRepositoryResult<Calendar>;
/// Lists all calendars for a specific user
async fn list_calendars_by_owner(&self, owner_id: &str) -> CalendarRepositoryResult<Vec<Calendar>>;
async fn list_calendars_by_owner(
&self,
owner_id: &str,
) -> CalendarRepositoryResult<Vec<Calendar>>;
/// Finds a calendar by name and owner
async fn find_calendar_by_name_and_owner(&self, name: &str, owner_id: &str) -> CalendarRepositoryResult<Calendar>;
async fn find_calendar_by_name_and_owner(
&self,
name: &str,
owner_id: &str,
) -> CalendarRepositoryResult<Calendar>;
/// Lists calendars shared with a specific user
async fn list_calendars_shared_with_user(&self, user_id: &str) -> CalendarRepositoryResult<Vec<Calendar>>;
async fn list_calendars_shared_with_user(
&self,
user_id: &str,
) -> CalendarRepositoryResult<Vec<Calendar>>;
/// List public calendars
async fn list_public_calendars(&self, limit: i64, offset: i64) -> CalendarRepositoryResult<Vec<Calendar>>;
async fn list_public_calendars(
&self,
limit: i64,
offset: i64,
) -> CalendarRepositoryResult<Vec<Calendar>>;
/// Checks if a user has access to a calendar
async fn user_has_calendar_access(&self, calendar_id: &Uuid, user_id: &str) -> CalendarRepositoryResult<bool>;
async fn user_has_calendar_access(
&self,
calendar_id: &Uuid,
user_id: &str,
) -> CalendarRepositoryResult<bool>;
/// Gets a custom property for a calendar
async fn get_calendar_property(&self, calendar_id: &Uuid, property_name: &str) -> CalendarRepositoryResult<Option<String>>;
async fn get_calendar_property(
&self,
calendar_id: &Uuid,
property_name: &str,
) -> CalendarRepositoryResult<Option<String>>;
/// Sets a custom property for a calendar
async fn set_calendar_property(&self, calendar_id: &Uuid, property_name: &str, property_value: &str) -> CalendarRepositoryResult<()>;
async fn set_calendar_property(
&self,
calendar_id: &Uuid,
property_name: &str,
property_value: &str,
) -> CalendarRepositoryResult<()>;
/// Removes a custom property from a calendar
async fn remove_calendar_property(&self, calendar_id: &Uuid, property_name: &str) -> CalendarRepositoryResult<()>;
async fn remove_calendar_property(
&self,
calendar_id: &Uuid,
property_name: &str,
) -> CalendarRepositoryResult<()>;
/// Gets all custom properties for a calendar
async fn get_calendar_properties(&self, calendar_id: &Uuid) -> CalendarRepositoryResult<std::collections::HashMap<String, String>>;
async fn get_calendar_properties(
&self,
calendar_id: &Uuid,
) -> CalendarRepositoryResult<std::collections::HashMap<String, String>>;
/// Share calendar with another user
async fn share_calendar(&self, calendar_id: &Uuid, user_id: &str, access_level: &str) -> CalendarRepositoryResult<()>;
async fn share_calendar(
&self,
calendar_id: &Uuid,
user_id: &str,
access_level: &str,
) -> CalendarRepositoryResult<()>;
/// Remove calendar sharing for a user
async fn remove_calendar_sharing(&self, calendar_id: &Uuid, user_id: &str) -> CalendarRepositoryResult<()>;
async fn remove_calendar_sharing(
&self,
calendar_id: &Uuid,
user_id: &str,
) -> CalendarRepositoryResult<()>;
/// Get calendar sharing information (who has access to this calendar)
async fn get_calendar_shares(&self, calendar_id: &Uuid) -> CalendarRepositoryResult<Vec<(String, String)>>;
}
async fn get_calendar_shares(
&self,
calendar_id: &Uuid,
) -> CalendarRepositoryResult<Vec<(String, String)>>;
}
+38 -11
View File
@@ -1,6 +1,6 @@
use async_trait::async_trait;
use uuid::Uuid;
use std::result::Result;
use uuid::Uuid;
use crate::common::errors::DomainError;
use crate::domain::entities::contact::{Contact, ContactGroup};
@@ -13,11 +13,23 @@ pub trait ContactRepository: Send + Sync + 'static {
async fn update_contact(&self, contact: Contact) -> ContactRepositoryResult<Contact>;
async fn delete_contact(&self, id: &Uuid) -> ContactRepositoryResult<()>;
async fn get_contact_by_id(&self, id: &Uuid) -> ContactRepositoryResult<Option<Contact>>;
async fn get_contact_by_uid(&self, address_book_id: &Uuid, uid: &str) -> ContactRepositoryResult<Option<Contact>>;
async fn get_contacts_by_address_book(&self, address_book_id: &Uuid) -> ContactRepositoryResult<Vec<Contact>>;
async fn get_contact_by_uid(
&self,
address_book_id: &Uuid,
uid: &str,
) -> ContactRepositoryResult<Option<Contact>>;
async fn get_contacts_by_address_book(
&self,
address_book_id: &Uuid,
) -> ContactRepositoryResult<Vec<Contact>>;
async fn get_contacts_by_email(&self, email: &str) -> ContactRepositoryResult<Vec<Contact>>;
async fn get_contacts_by_group(&self, group_id: &Uuid) -> ContactRepositoryResult<Vec<Contact>>;
async fn search_contacts(&self, address_book_id: &Uuid, query: &str) -> ContactRepositoryResult<Vec<Contact>>;
async fn get_contacts_by_group(&self, group_id: &Uuid)
-> ContactRepositoryResult<Vec<Contact>>;
async fn search_contacts(
&self,
address_book_id: &Uuid,
query: &str,
) -> ContactRepositoryResult<Vec<Contact>>;
}
#[async_trait]
@@ -26,9 +38,24 @@ pub trait ContactGroupRepository: Send + Sync + 'static {
async fn update_group(&self, group: ContactGroup) -> ContactRepositoryResult<ContactGroup>;
async fn delete_group(&self, id: &Uuid) -> ContactRepositoryResult<()>;
async fn get_group_by_id(&self, id: &Uuid) -> ContactRepositoryResult<Option<ContactGroup>>;
async fn get_groups_by_address_book(&self, address_book_id: &Uuid) -> ContactRepositoryResult<Vec<ContactGroup>>;
async fn add_contact_to_group(&self, group_id: &Uuid, contact_id: &Uuid) -> ContactRepositoryResult<()>;
async fn remove_contact_from_group(&self, group_id: &Uuid, contact_id: &Uuid) -> ContactRepositoryResult<()>;
async fn get_contacts_in_group(&self, group_id: &Uuid) -> ContactRepositoryResult<Vec<Contact>>;
async fn get_groups_for_contact(&self, contact_id: &Uuid) -> ContactRepositoryResult<Vec<ContactGroup>>;
}
async fn get_groups_by_address_book(
&self,
address_book_id: &Uuid,
) -> ContactRepositoryResult<Vec<ContactGroup>>;
async fn add_contact_to_group(
&self,
group_id: &Uuid,
contact_id: &Uuid,
) -> ContactRepositoryResult<()>;
async fn remove_contact_from_group(
&self,
group_id: &Uuid,
contact_id: &Uuid,
) -> ContactRepositoryResult<()>;
async fn get_contacts_in_group(&self, group_id: &Uuid)
-> ContactRepositoryResult<Vec<Contact>>;
async fn get_groups_for_contact(
&self,
contact_id: &Uuid,
) -> ContactRepositoryResult<Vec<ContactGroup>>;
}
+9 -8
View File
@@ -15,9 +15,9 @@ use async_trait::async_trait;
use bytes::Bytes;
use futures::Stream;
use crate::common::errors::DomainError;
use crate::domain::entities::file::File;
use crate::domain::services::path_service::StoragePath;
use crate::common::errors::DomainError;
// ─────────────────────────────────────────────────────
// FileReadRepository — read/query operations
@@ -98,17 +98,14 @@ pub trait FileWriteRepository: Send + Sync + 'static {
) -> Result<File, DomainError>;
/// Renames a file (same folder, different name).
async fn rename_file(
&self,
file_id: &str,
new_name: &str,
) -> Result<File, DomainError>;
async fn rename_file(&self, file_id: &str, new_name: &str) -> Result<File, DomainError>;
/// Deletes a file.
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
/// Updates the content of an existing file.
async fn update_file_content(&self, file_id: &str, content: Vec<u8>) -> Result<(), DomainError>;
async fn update_file_content(&self, file_id: &str, content: Vec<u8>)
-> Result<(), DomainError>;
/// Registers file metadata WITHOUT writing content to disk (write-behind).
///
@@ -128,7 +125,11 @@ pub trait FileWriteRepository: Send + Sync + 'static {
async fn move_to_trash(&self, file_id: &str) -> Result<(), DomainError>;
/// Restores a file from the trash to its original location
async fn restore_from_trash(&self, file_id: &str, original_path: &str) -> Result<(), DomainError>;
async fn restore_from_trash(
&self,
file_id: &str,
original_path: &str,
) -> Result<(), DomainError>;
/// Permanently deletes a file (used by the trash)
async fn delete_file_permanently(&self, file_id: &str) -> Result<(), DomainError>;
+27 -15
View File
@@ -10,9 +10,9 @@
use async_trait::async_trait;
use crate::common::errors::DomainError;
use crate::domain::entities::folder::Folder;
use crate::domain::services::path_service::StoragePath;
use crate::common::errors::DomainError;
/// Domain port for folder persistence.
///
@@ -21,38 +21,46 @@ use crate::common::errors::DomainError;
#[async_trait]
pub trait FolderRepository: Send + Sync + 'static {
/// Creates a new folder
async fn create_folder(&self, name: String, parent_id: Option<String>) -> Result<Folder, DomainError>;
async fn create_folder(
&self,
name: String,
parent_id: Option<String>,
) -> Result<Folder, DomainError>;
/// Gets a folder by its ID
async fn get_folder(&self, id: &str) -> Result<Folder, DomainError>;
/// Gets a folder by its storage path
async fn get_folder_by_path(&self, storage_path: &StoragePath) -> Result<Folder, DomainError>;
/// Lists folders within a parent folder
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<Folder>, DomainError>;
/// Lists folders with pagination
async fn list_folders_paginated(
&self,
&self,
parent_id: Option<&str>,
offset: usize,
limit: usize,
include_total: bool
include_total: bool,
) -> Result<(Vec<Folder>, Option<usize>), DomainError>;
/// Renames a folder
async fn rename_folder(&self, id: &str, new_name: String) -> Result<Folder, DomainError>;
/// Moves a folder to another parent
async fn move_folder(&self, id: &str, new_parent_id: Option<&str>) -> Result<Folder, DomainError>;
async fn move_folder(
&self,
id: &str,
new_parent_id: Option<&str>,
) -> Result<Folder, DomainError>;
/// Deletes a folder
async fn delete_folder(&self, id: &str) -> Result<(), DomainError>;
/// Checks if a folder exists at the given path
async fn folder_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
/// Gets the path of a folder
async fn get_folder_path(&self, id: &str) -> Result<StoragePath, DomainError>;
@@ -62,7 +70,11 @@ pub trait FolderRepository: Send + Sync + 'static {
async fn move_to_trash(&self, folder_id: &str) -> Result<(), DomainError>;
/// Restores a folder from the trash to its original location
async fn restore_from_trash(&self, folder_id: &str, original_path: &str) -> Result<(), DomainError>;
async fn restore_from_trash(
&self,
folder_id: &str,
original_path: &str,
) -> Result<(), DomainError>;
/// Permanently deletes a folder (used by the trash)
async fn delete_folder_permanently(&self, folder_id: &str) -> Result<(), DomainError>;
+3 -3
View File
@@ -1,11 +1,11 @@
pub mod address_book_repository;
pub mod calendar_repository;
pub mod calendar_event_repository;
pub mod calendar_repository;
pub mod contact_repository;
pub mod file_repository;
pub mod folder_repository;
pub mod session_repository;
pub mod settings_repository;
pub mod share_repository;
pub mod trash_repository;
pub mod settings_repository;
pub mod user_repository;
pub mod user_repository;
+20 -20
View File
@@ -1,15 +1,15 @@
use async_trait::async_trait;
use crate::domain::entities::session::Session;
use crate::common::errors::DomainError;
use crate::domain::entities::session::Session;
use async_trait::async_trait;
#[derive(Debug, thiserror::Error)]
pub enum SessionRepositoryError {
#[error("Session not found: {0}")]
NotFound(String),
#[error("Database error: {0}")]
DatabaseError(String),
#[error("Timeout error: {0}")]
Timeout(String),
}
@@ -20,15 +20,11 @@ pub type SessionRepositoryResult<T> = Result<T, SessionRepositoryError>;
impl From<SessionRepositoryError> for DomainError {
fn from(err: SessionRepositoryError) -> Self {
match err {
SessionRepositoryError::NotFound(msg) => {
DomainError::not_found("Session", msg)
},
SessionRepositoryError::NotFound(msg) => DomainError::not_found("Session", msg),
SessionRepositoryError::DatabaseError(msg) => {
DomainError::internal_error("Database", msg)
},
SessionRepositoryError::Timeout(msg) => {
DomainError::timeout("Database", msg)
},
}
SessionRepositoryError::Timeout(msg) => DomainError::timeout("Database", msg),
}
}
}
@@ -37,22 +33,26 @@ impl From<SessionRepositoryError> for DomainError {
pub trait SessionRepository: Send + Sync + 'static {
/// Creates a new session
async fn create_session(&self, session: Session) -> SessionRepositoryResult<Session>;
/// Gets a session by ID
async fn get_session_by_id(&self, id: &str) -> SessionRepositoryResult<Session>;
/// Gets a session by refresh token
async fn get_session_by_refresh_token(&self, refresh_token: &str) -> SessionRepositoryResult<Session>;
async fn get_session_by_refresh_token(
&self,
refresh_token: &str,
) -> SessionRepositoryResult<Session>;
/// Gets all sessions for a user
async fn get_sessions_by_user_id(&self, user_id: &str) -> SessionRepositoryResult<Vec<Session>>;
async fn get_sessions_by_user_id(&self, user_id: &str)
-> SessionRepositoryResult<Vec<Session>>;
/// Revokes a specific session
async fn revoke_session(&self, session_id: &str) -> SessionRepositoryResult<()>;
/// Revokes all sessions for a user
async fn revoke_all_user_sessions(&self, user_id: &str) -> SessionRepositoryResult<u64>;
/// Deletes expired sessions
async fn delete_expired_sessions(&self) -> SessionRepositoryResult<u64>;
}
}
+28 -27
View File
@@ -1,27 +1,28 @@
use std::collections::HashMap;
use async_trait::async_trait;
use crate::common::errors::DomainError;
/// Repository for platform settings stored in the database.
/// Settings are key-value pairs organized by category (e.g., "oidc", "general").
#[async_trait]
pub trait SettingsRepository: Send + Sync + 'static {
/// Get a single setting value by key
async fn get(&self, key: &str) -> Result<Option<String>, DomainError>;
/// Get all settings for a given category
async fn get_by_category(&self, category: &str) -> Result<HashMap<String, String>, DomainError>;
/// Set a setting value (upsert)
async fn set(
&self,
key: &str,
value: &str,
category: &str,
is_secret: bool,
updated_by: Option<&str>,
) -> Result<(), DomainError>;
/// Delete a setting by key
async fn delete(&self, key: &str) -> Result<(), DomainError>;
}
use crate::common::errors::DomainError;
use async_trait::async_trait;
use std::collections::HashMap;
/// Repository for platform settings stored in the database.
/// Settings are key-value pairs organized by category (e.g., "oidc", "general").
#[async_trait]
pub trait SettingsRepository: Send + Sync + 'static {
/// Get a single setting value by key
async fn get(&self, key: &str) -> Result<Option<String>, DomainError>;
/// Get all settings for a given category
async fn get_by_category(&self, category: &str)
-> Result<HashMap<String, String>, DomainError>;
/// Set a setting value (upsert)
async fn set(
&self,
key: &str,
value: &str,
category: &str,
is_secret: bool,
updated_by: Option<&str>,
) -> Result<(), DomainError>;
/// Delete a setting by key
async fn delete(&self, key: &str) -> Result<(), DomainError>;
}
+11 -8
View File
@@ -1,4 +1,3 @@
use async_trait::async_trait;
use thiserror::Error;
@@ -25,22 +24,26 @@ pub enum ShareRepositoryError {
pub trait ShareRepository: Send + Sync + 'static {
/// Save a new share or update an existing one
async fn save(&self, share: &Share) -> Result<Share, ShareRepositoryError>;
/// Find a share by its ID
async fn find_by_id(&self, id: &str) -> Result<Share, ShareRepositoryError>;
/// Find a share by its token
async fn find_by_token(&self, token: &str) -> Result<Share, ShareRepositoryError>;
/// Find all shares for a specific item
async fn find_by_item(&self, item_id: &str, item_type: &ShareItemType) -> Result<Vec<Share>, ShareRepositoryError>;
async fn find_by_item(
&self,
item_id: &str,
item_type: &ShareItemType,
) -> Result<Vec<Share>, ShareRepositoryError>;
/// Delete a share by its ID
async fn delete(&self, id: &str) -> Result<(), ShareRepositoryError>;
/// Find all shares created by a specific user
async fn find_by_user(&self, user_id: &str) -> Result<Vec<Share>, ShareRepositoryError>;
/// Find all shares (admin operation)
async fn find_all(&self) -> Result<Vec<Share>, ShareRepositoryError>;
}
+2 -2
View File
@@ -1,8 +1,8 @@
use async_trait::async_trait;
use uuid::Uuid;
use crate::domain::entities::trashed_item::TrashedItem;
use crate::common::errors::Result;
use crate::domain::entities::trashed_item::TrashedItem;
#[async_trait]
pub trait TrashRepository: Send + Sync {
@@ -13,4 +13,4 @@ pub trait TrashRepository: Send + Sync {
async fn delete_permanently(&self, id: &Uuid, user_id: &Uuid) -> Result<()>;
async fn clear_trash(&self, user_id: &Uuid) -> Result<()>;
async fn get_expired_items(&self) -> Result<Vec<TrashedItem>>;
}
}
+45 -41
View File
@@ -1,24 +1,24 @@
use async_trait::async_trait;
use crate::domain::entities::user::{User, UserRole};
use crate::common::errors::DomainError;
use crate::domain::entities::user::{User, UserRole};
use async_trait::async_trait;
#[derive(Debug, thiserror::Error)]
pub enum UserRepositoryError {
#[error("User not found: {0}")]
NotFound(String),
#[error("User already exists: {0}")]
AlreadyExists(String),
#[error("Database error: {0}")]
DatabaseError(String),
#[error("Validation error: {0}")]
ValidationError(String),
#[error("Timeout error: {0}")]
Timeout(String),
#[error("Operation not allowed: {0}")]
OperationNotAllowed(String),
}
@@ -29,24 +29,14 @@ pub type UserRepositoryResult<T> = Result<T, UserRepositoryError>;
impl From<UserRepositoryError> for DomainError {
fn from(err: UserRepositoryError) -> Self {
match err {
UserRepositoryError::NotFound(msg) => {
DomainError::not_found("User", msg)
},
UserRepositoryError::AlreadyExists(msg) => {
DomainError::already_exists("User", msg)
},
UserRepositoryError::DatabaseError(msg) => {
DomainError::internal_error("Database", msg)
},
UserRepositoryError::ValidationError(msg) => {
DomainError::validation_error(msg)
},
UserRepositoryError::Timeout(msg) => {
DomainError::timeout("Database", msg)
},
UserRepositoryError::NotFound(msg) => DomainError::not_found("User", msg),
UserRepositoryError::AlreadyExists(msg) => DomainError::already_exists("User", msg),
UserRepositoryError::DatabaseError(msg) => DomainError::internal_error("Database", msg),
UserRepositoryError::ValidationError(msg) => DomainError::validation_error(msg),
UserRepositoryError::Timeout(msg) => DomainError::timeout("Database", msg),
UserRepositoryError::OperationNotAllowed(msg) => {
DomainError::access_denied("User", msg)
},
}
}
}
}
@@ -55,48 +45,62 @@ impl From<UserRepositoryError> for DomainError {
pub trait UserRepository: Send + Sync + 'static {
/// Creates a new user
async fn create_user(&self, user: User) -> UserRepositoryResult<User>;
/// Gets a user by ID
async fn get_user_by_id(&self, id: &str) -> UserRepositoryResult<User>;
/// Gets a user by username
async fn get_user_by_username(&self, username: &str) -> UserRepositoryResult<User>;
/// Gets a user by email
async fn get_user_by_email(&self, email: &str) -> UserRepositoryResult<User>;
/// Updates an existing user
async fn update_user(&self, user: User) -> UserRepositoryResult<User>;
/// Updates only a user's storage usage
async fn update_storage_usage(&self, user_id: &str, usage_bytes: i64) -> UserRepositoryResult<()>;
async fn update_storage_usage(
&self,
user_id: &str,
usage_bytes: i64,
) -> UserRepositoryResult<()>;
/// Updates the last login date
async fn update_last_login(&self, user_id: &str) -> UserRepositoryResult<()>;
/// Lists users with pagination
async fn list_users(&self, limit: i64, offset: i64) -> UserRepositoryResult<Vec<User>>;
/// Activates or deactivates a user
async fn set_user_active_status(&self, user_id: &str, active: bool) -> UserRepositoryResult<()>;
async fn set_user_active_status(&self, user_id: &str, active: bool)
-> UserRepositoryResult<()>;
/// Changes a user's password
async fn change_password(&self, user_id: &str, password_hash: &str) -> UserRepositoryResult<()>;
async fn change_password(&self, user_id: &str, password_hash: &str)
-> UserRepositoryResult<()>;
/// Changes a user's role
async fn change_role(&self, user_id: &str, role: UserRole) -> UserRepositoryResult<()>;
/// Lists users by role (admin or user)
async fn list_users_by_role(&self, role: &str) -> UserRepositoryResult<Vec<User>>;
/// Deletes a user
async fn delete_user(&self, user_id: &str) -> UserRepositoryResult<()>;
/// Finds a user by OIDC provider + subject pair
async fn get_user_by_oidc_subject(&self, provider: &str, subject: &str) -> UserRepositoryResult<User>;
async fn get_user_by_oidc_subject(
&self,
provider: &str,
subject: &str,
) -> UserRepositoryResult<User>;
/// Updates a user's storage quota
async fn update_storage_quota(&self, user_id: &str, quota_bytes: i64) -> UserRepositoryResult<()>;
async fn update_storage_quota(
&self,
user_id: &str,
quota_bytes: i64,
) -> UserRepositoryResult<()>;
/// Counts the total number of users
async fn count_users(&self) -> UserRepositoryResult<i64>;
@@ -114,4 +118,4 @@ pub struct StorageStats {
pub total_used_bytes: i64,
pub users_over_80_percent: i64,
pub users_over_quota: i64,
}
}
+8 -8
View File
@@ -6,10 +6,10 @@ use thiserror::Error;
pub enum I18nError {
#[error("Translation key not found: {0}")]
KeyNotFound(String),
#[error("Invalid locale: {0}")]
InvalidLocale(String),
#[error("Error loading translations: {0}")]
LoadError(String),
}
@@ -38,7 +38,7 @@ impl Locale {
Locale::Portuguese => "pt",
}
}
/// Create from locale code string
pub fn from_str(code: &str) -> Option<Self> {
match code.to_lowercase().as_str() {
@@ -50,7 +50,7 @@ impl Locale {
_ => None,
}
}
/// Get default locale
pub fn default() -> Self {
Locale::English
@@ -62,13 +62,13 @@ impl Locale {
pub trait I18nService: Send + Sync + 'static {
/// Get a translation for a key and locale
async fn translate(&self, key: &str, locale: Locale) -> I18nResult<String>;
/// Load translations for a locale
async fn load_translations(&self, locale: Locale) -> I18nResult<()>;
/// Get available locales
async fn available_locales(&self) -> Vec<Locale>;
/// Check if a locale is supported
async fn is_supported(&self, locale: Locale) -> bool;
}
}
+1 -1
View File
@@ -2,4 +2,4 @@ pub mod i18n_service;
pub mod path_service;
// NOTE: auth_service has been moved to infrastructure/services/jwt_service.rs
// The functionality is now exposed through application/ports/auth_ports.rs (TokenServicePort)
// The functionality is now exposed through application/ports/auth_ports.rs (TokenServicePort)
+27 -21
View File
@@ -1,7 +1,7 @@
//! StoragePath - Domain Value Object for representing storage paths
//!
//!
//! This module contains only the StoragePath Value Object which is part of the pure domain.
//! PathService (which implements StoragePort and StorageMediator) was moved to
//! PathService (which implements StoragePort and StorageMediator) was moved to
//! infrastructure/services/path_service.rs because it has file system dependencies.
use std::path::PathBuf;
@@ -17,12 +17,14 @@ impl StoragePath {
pub fn new(segments: Vec<String>) -> Self {
Self { segments }
}
/// Creates an empty path (root)
pub fn root() -> Self {
Self { segments: Vec::new() }
Self {
segments: Vec::new(),
}
}
/// Creates a path from a string with segments separated by /
pub fn from_string(path: &str) -> Self {
let segments = path
@@ -32,7 +34,7 @@ impl StoragePath {
.collect();
Self { segments }
}
/// Creates a path from a PathBuf
pub fn from(path_buf: PathBuf) -> Self {
let segments = path_buf
@@ -44,34 +46,38 @@ impl StoragePath {
.collect();
Self { segments }
}
/// Appends a segment to the path
pub fn join(&self, segment: &str) -> Self {
let mut new_segments = self.segments.clone();
new_segments.push(segment.to_string());
Self { segments: new_segments }
Self {
segments: new_segments,
}
}
/// Gets the file name (last segment)
pub fn file_name(&self) -> Option<String> {
self.segments.last().cloned()
}
/// Gets the parent directory path
pub fn parent(&self) -> Option<Self> {
if self.segments.is_empty() {
None
} else {
let parent_segments = self.segments[..self.segments.len() - 1].to_vec();
Some(Self { segments: parent_segments })
Some(Self {
segments: parent_segments,
})
}
}
/// Checks if the path is empty (is the root)
pub fn is_empty(&self) -> bool {
self.segments.is_empty()
}
/// Converts the path to a string with format "/segment1/segment2/..."
pub fn to_string(&self) -> String {
if self.segments.is_empty() {
@@ -80,7 +86,7 @@ impl StoragePath {
format!("/{}", self.segments.join("/"))
}
}
/// Returns the path representation as a string
pub fn as_str(&self) -> &str {
// Note: The implementation should really store the string,
@@ -88,7 +94,7 @@ impl StoragePath {
// This is only used for the get_folder_path_str implementation
"/"
}
/// Gets the path segments
pub fn segments(&self) -> &[String] {
&self.segments
@@ -98,38 +104,38 @@ impl StoragePath {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_storage_path_from_string() {
let path = StoragePath::from_string("folder/subfolder/file.txt");
assert_eq!(path.segments(), &["folder", "subfolder", "file.txt"]);
assert_eq!(path.to_string(), "/folder/subfolder/file.txt");
}
#[test]
fn test_storage_path_join() {
let path = StoragePath::from_string("folder");
let joined = path.join("file.txt");
assert_eq!(joined.to_string(), "/folder/file.txt");
}
#[test]
fn test_storage_path_parent() {
let path = StoragePath::from_string("folder/file.txt");
let parent = path.parent().unwrap();
assert_eq!(parent.to_string(), "/folder");
}
#[test]
fn test_storage_path_root() {
let root = StoragePath::root();
assert!(root.is_empty());
assert_eq!(root.to_string(), "/");
}
#[test]
fn test_storage_path_file_name() {
let path = StoragePath::from_string("folder/file.txt");
assert_eq!(path.file_name(), Some("file.txt".to_string()));
}
}
}