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:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -103,4 +103,4 @@ impl TrashedItem {
|
||||
let now = Utc::now();
|
||||
(self.deletion_date - now).num_days().max(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+36
-34
@@ -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
@@ -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>>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)>>;
|
||||
}
|
||||
|
||||
@@ -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>>;
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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>>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user