aboutsummaryrefslogtreecommitdiff
path: root/src/domain
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/domain/calendar.rs76
-rw-r--r--src/domain/error.rs28
-rw-r--r--src/domain/event.rs116
-rw-r--r--src/domain/mod.rs7
-rw-r--r--src/domain/recurrence.rs200
-rw-r--r--src/domain/repository.rs45
-rwxr-xr-xsrc/domain/value_objects.rs147
7 files changed, 619 insertions, 0 deletions
diff --git a/src/domain/calendar.rs b/src/domain/calendar.rs
new file mode 100644
index 0000000..21216b5
--- /dev/null
+++ b/src/domain/calendar.rs
@@ -0,0 +1,76 @@
+use chrono::{DateTime, Utc};
+use getset::Getters;
+
+use crate::domain::value_objects::CalendarId;
+
+#[derive(Debug, Clone, Getters)]
+pub struct Calendar {
+ #[getset(get = "pub")]
+ id: CalendarId,
+ #[getset(get = "pub")]
+ name: String,
+ #[getset(get = "pub")]
+ description: Option<String>,
+ #[getset(get = "pub")]
+ is_archived: bool,
+ #[getset(get = "pub")]
+ created_at: DateTime<Utc>,
+ #[getset(get = "pub")]
+ updated_at: DateTime<Utc>,
+}
+
+impl Calendar {
+ pub fn new(name: String, description: Option<String>) -> Self {
+ let now = Utc::now();
+ Self {
+ id: CalendarId::new(),
+ name,
+ description,
+ is_archived: false,
+ created_at: now,
+ updated_at: now,
+ }
+ }
+
+ pub fn with_id(
+ id: CalendarId,
+ name: String,
+ description: Option<String>,
+ is_archived: bool,
+ created_at: DateTime<Utc>,
+ updated_at: DateTime<Utc>,
+ ) -> Self {
+ Self {
+ id,
+ name,
+ description,
+ is_archived,
+ created_at,
+ updated_at,
+ }
+ }
+
+ pub fn archive(&mut self) {
+ self.is_archived = true;
+ self.touch();
+ }
+
+ pub fn unarchive(&mut self) {
+ self.is_archived = false;
+ self.touch();
+ }
+
+ pub fn update_name(&mut self, name: String) {
+ self.name = name;
+ self.touch();
+ }
+
+ pub fn update_description(&mut self, description: Option<String>) {
+ self.description = description;
+ self.touch();
+ }
+
+ pub fn touch(&mut self) {
+ self.updated_at = Utc::now();
+ }
+}
diff --git a/src/domain/error.rs b/src/domain/error.rs
new file mode 100644
index 0000000..4602b7e
--- /dev/null
+++ b/src/domain/error.rs
@@ -0,0 +1,28 @@
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum DomainError {
+ #[error("Invalid time range: start time must be before end time")]
+ InvalidTimeRange,
+
+ #[error("Invalid color value")]
+ InvalidColor,
+
+ #[error("Invalid frequency")]
+ InvalidFrequency,
+
+ #[error("Invalid interval: must be greater than 0")]
+ InvalidInterval,
+
+ #[error("Calendar not found: {0}")]
+ CalendarNotFound(String),
+
+ #[error("Event not found: {0}")]
+ EventNotFound(String),
+
+ #[error("Recurrence not found: {0}")]
+ RecurrenceNotFound(String),
+
+ #[error("Cannot modify archived calendar")]
+ CalendarArchived,
+}
diff --git a/src/domain/event.rs b/src/domain/event.rs
new file mode 100644
index 0000000..93216a4
--- /dev/null
+++ b/src/domain/event.rs
@@ -0,0 +1,116 @@
+use chrono::{DateTime, Utc};
+use getset::Getters;
+
+use crate::domain::value_objects::{CalendarId, EventColor, EventId, TimeRange};
+
+#[derive(Debug, Clone, Getters)]
+pub struct Event {
+ #[getset(get = "pub")]
+ id: EventId,
+ #[getset(get = "pub")]
+ calendar_id: CalendarId,
+ #[getset(get = "pub")]
+ title: String,
+ #[getset(get = "pub")]
+ description: Option<String>,
+ #[getset(get = "pub")]
+ time_range: TimeRange,
+ #[getset(get = "pub")]
+ color: EventColor,
+ #[getset(get = "pub")]
+ is_all_day: bool,
+ #[getset(get = "pub")]
+ is_cancelled: bool,
+ #[getset(get = "pub")]
+ created_at: DateTime<Utc>,
+ #[getset(get = "pub")]
+ updated_at: DateTime<Utc>,
+}
+
+impl Event {
+ pub fn new(
+ calendar_id: CalendarId,
+ title: String,
+ description: Option<String>,
+ time_range: TimeRange,
+ color: EventColor,
+ is_all_day: bool,
+ ) -> Self {
+ let now = Utc::now();
+ Self {
+ id: EventId::new(),
+ calendar_id,
+ title,
+ description,
+ time_range,
+ color,
+ is_all_day,
+ is_cancelled: false,
+ created_at: now,
+ updated_at: now,
+ }
+ }
+
+ pub fn with_id(
+ id: EventId,
+ calendar_id: CalendarId,
+ title: String,
+ description: Option<String>,
+ time_range: TimeRange,
+ color: EventColor,
+ is_all_day: bool,
+ is_cancelled: bool,
+ created_at: DateTime<Utc>,
+ updated_at: DateTime<Utc>,
+ ) -> Self {
+ Self {
+ id,
+ calendar_id,
+ title,
+ description,
+ time_range,
+ color,
+ is_all_day,
+ is_cancelled,
+ created_at,
+ updated_at,
+ }
+ }
+
+ pub fn cancel(&mut self) {
+ self.is_cancelled = true;
+ self.updated_at = Utc::now();
+ }
+
+ pub fn restore(&mut self) {
+ self.is_cancelled = false;
+ self.updated_at = Utc::now();
+ }
+
+ pub fn update_title(&mut self, title: String) {
+ self.title = title;
+ self.updated_at = Utc::now();
+ }
+
+ pub fn update_description(&mut self, description: Option<String>) {
+ self.description = description;
+ self.updated_at = Utc::now();
+ }
+
+ pub fn update_time_range(&mut self, time_range: TimeRange) {
+ self.time_range = time_range;
+ self.updated_at = Utc::now();
+ }
+
+ pub fn update_color(&mut self, color: EventColor) {
+ self.color = color;
+ self.updated_at = Utc::now();
+ }
+
+ pub fn overlaps_with(&self, other: &Event) -> bool {
+ !self.is_cancelled
+ && !other.is_cancelled
+ && self.calendar_id == other.calendar_id
+ && self.time_range.overlaps(other.time_range())
+ }
+}
diff --git a/src/domain/mod.rs b/src/domain/mod.rs
new file mode 100644
index 0000000..05226bf
--- /dev/null
+++ b/src/domain/mod.rs
@@ -0,0 +1,7 @@
+pub mod calendar;
+pub mod event;
+pub mod recurrence;
+pub mod value_objects;
+pub mod repository;
+
+pub mod error;
diff --git a/src/domain/recurrence.rs b/src/domain/recurrence.rs
new file mode 100644
index 0000000..263868e
--- /dev/null
+++ b/src/domain/recurrence.rs
@@ -0,0 +1,200 @@
+use std::collections::HashMap;
+
+use chrono::{DateTime, Utc};
+use getset::Getters;
+
+use crate::domain::{
+ error::DomainError,
+ value_objects::{CalendarId, EventColor, EventId, Frequency, TimeRange}
+};
+
+#[derive(Debug, Clone, Getters)]
+pub struct RecurrenceRule {
+ #[getset(get = "pub")]
+ frequency: Frequency,
+ #[getset(get = "pub")]
+ interval: u32,
+ #[getset(get = "pub")]
+ until: Option<DateTime<Utc>>,
+}
+
+impl RecurrenceRule {
+ pub fn new(
+ frequency: Frequency,
+ interval: u32,
+ until: Option<DateTime<Utc>>,
+ ) -> Result<Self, DomainError> {
+ if interval == 0 {
+ return Err(DomainError::InvalidInterval);
+ }
+ Ok(Self {
+ frequency,
+ interval,
+ until,
+ })
+ }
+}
+
+#[derive(Debug, Clone, Getters)]
+pub struct RecurringEvent {
+ #[getset(get = "pub")]
+ id: EventId,
+ #[getset(get = "pub")]
+ calendar_id: CalendarId,
+ #[getset(get = "pub")]
+ title: String,
+ #[getset(get = "pub")]
+ description: Option<String>,
+ #[getset(get = "pub")]
+ time_range: TimeRange,
+ #[getset(get = "pub")]
+ rule: RecurrenceRule,
+ #[getset(get = "pub")]
+ exceptions: HashMap<DateTime<Utc>, RecurrenceException>,
+ #[getset(get = "pub")]
+ color: EventColor,
+ #[getset(get = "pub")]
+ is_all_day: bool,
+ #[getset(get = "pub")]
+ is_cancelled: bool,
+ #[getset(get = "pub")]
+ created_at: DateTime<Utc>,
+ #[getset(get = "pub")]
+ updated_at: DateTime<Utc>,
+}
+
+impl RecurringEvent {
+ pub fn new(
+ calendar_id: CalendarId,
+ title: String,
+ description: Option<String>,
+ time_range: TimeRange,
+ rule: RecurrenceRule,
+ color: EventColor,
+ is_all_day: bool,
+ ) -> Self {
+ let now = Utc::now();
+ Self {
+ id: EventId::new(),
+ calendar_id,
+ title,
+ description,
+ time_range,
+ rule,
+ exceptions: HashMap::new(),
+ color,
+ is_all_day,
+ is_cancelled: false,
+ created_at: now,
+ updated_at: now,
+ }
+ }
+
+ pub fn with_id(
+ id: EventId,
+ calendar_id: CalendarId,
+ title: String,
+ description: Option<String>,
+ time_range: TimeRange,
+ rule: RecurrenceRule,
+ exceptions: HashMap<DateTime<Utc>, RecurrenceException>,
+ color: EventColor,
+ is_all_day: bool,
+ is_cancelled: bool,
+ created_at: DateTime<Utc>,
+ updated_at: DateTime<Utc>,
+ ) -> Self {
+ Self {
+ id,
+ calendar_id,
+ title,
+ description,
+ time_range,
+ rule,
+ exceptions,
+ color,
+ is_all_day,
+ is_cancelled,
+ created_at,
+ updated_at,
+ }
+ }
+
+ pub fn add_exception(&mut self, exception: RecurrenceException) {
+ self.exceptions.insert(exception.original_starts_at, exception);
+ self.touch();
+ }
+
+ pub fn remove_exception(&mut self, original_starts_at: DateTime<Utc>) {
+ self.exceptions.remove(&original_starts_at);
+ self.touch();
+ }
+
+ pub fn cancel_occurrence(&mut self, original_starts_at: DateTime<Utc>) {
+ let exception = RecurrenceException::cancelled(original_starts_at);
+ self.add_exception(exception);
+ }
+
+ pub fn reschedule_occurrence(
+ &mut self,
+ original_starts_at: DateTime<Utc>,
+ new_time_range: TimeRange,
+ ) {
+ let exception = RecurrenceException::rescheduled(
+ original_starts_at,
+ new_time_range
+ );
+ self.add_exception(exception);
+ }
+
+ pub fn cancel(&mut self) {
+ self.is_cancelled = true;
+ self.touch();
+ }
+
+ pub fn touch(&mut self) {
+ self.updated_at = Utc::now();
+ }
+}
+
+#[derive(Debug, Clone, Getters)]
+pub struct RecurrenceException {
+ #[getset(get = "pub")]
+ original_starts_at: DateTime<Utc>,
+ #[getset(get = "pub")]
+ modification: ExceptionModification,
+}
+
+#[derive(Debug, Clone)]
+pub enum ExceptionModification {
+ Cancelled,
+ Rescheduled { new_time_range: TimeRange },
+}
+
+impl RecurrenceException {
+ pub fn cancelled(original_starts_at: DateTime<Utc>) -> Self {
+ Self {
+ original_starts_at,
+ modification: ExceptionModification::Cancelled,
+ }
+ }
+
+ pub fn rescheduled(
+ original_starts_at: DateTime<Utc>,
+ new_time_range: TimeRange,
+ ) -> Self {
+ Self {
+ original_starts_at,
+ modification: ExceptionModification::Rescheduled { new_time_range },
+ }
+ }
+
+ pub fn new_time_range(&self) -> Option<&TimeRange> {
+ match &self.modification {
+ ExceptionModification::Rescheduled {
+ new_time_range,
+ } => Some(new_time_range),
+ ExceptionModification::Cancelled => None,
+ }
+ }
+}
diff --git a/src/domain/repository.rs b/src/domain/repository.rs
new file mode 100644
index 0000000..00b3974
--- /dev/null
+++ b/src/domain/repository.rs
@@ -0,0 +1,45 @@
+use async_trait::async_trait;
+use super::{
+ calendar::Calendar,
+ event::Event,
+ recurrence::RecurringEvent,
+ value_objects::{CalendarId, EventId, TimeRange},
+};
+
+pub type Result<T> = std::result::Result<T, RepositoryError>;
+
+#[derive(Debug, thiserror::Error)]
+pub enum RepositoryError {
+ #[error("Entity not found")]
+ NotFound,
+
+ #[error("Database error: {0}")]
+ DatabaseError(String),
+
+ #[error("Constraint violation: {0}")]
+ ConstraintViolation(String),
+}
+
+#[async_trait]
+pub trait CalendarRepository: Send + Sync {
+ async fn save(&self, calendar: &Calendar) -> Result<()>;
+ async fn find_by_id(&self, id: &CalendarId) -> Result<Option<Calendar>>;
+ async fn find_all_active(&self) -> Result<Vec<Calendar>>;
+ async fn delete(&self, id: &CalendarId) -> Result<()>;
+}
+
+#[async_trait]
+pub trait EventRepository: Send + Sync {
+ async fn save(&self, event: &Event) -> Result<()>;
+ async fn find_by_id(&self, id: &EventId) -> Result<Option<Event>>;
+ async fn find_by_calendar(&self, calendar_id: &CalendarId) -> Result<Vec<Event>>;
+ async fn find_in_range(&self, calendar_id: &CalendarId, range: &TimeRange) -> Result<Vec<Event>>;
+ async fn delete(&self, id: &EventId) -> Result<()>;
+}
+
+#[async_trait]
+pub trait RecurringEventRepository: Send + Sync {
+ async fn save(&self, event: &RecurringEvent) -> Result<()>;
+ async fn find_by_calendar(&self, calendar_id: &CalendarId) -> Result<Vec<RecurringEvent>>;
+ async fn delete(&self, id: &str) -> Result<()>;
+}
diff --git a/src/domain/value_objects.rs b/src/domain/value_objects.rs
new file mode 100755
index 0000000..f38d351
--- /dev/null
+++ b/src/domain/value_objects.rs
@@ -0,0 +1,147 @@
+use core::fmt;
+
+use chrono::{DateTime, Utc};
+use getset::{Getters};
+use uuid::Uuid;
+
+use crate::domain::error::DomainError;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct EventColor(u8);
+
+impl From<u8> for EventColor {
+ fn from(value: u8) -> Self {
+ Self(value)
+ }
+}
+
+impl From<EventColor> for u8 {
+ fn from(color: EventColor) -> Self {
+ color.0
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Getters)]
+pub struct TimeRange {
+ #[getset(get = "pub")]
+ starts_at: DateTime<Utc>,
+ #[getset(get = "pub")]
+ ends_at: DateTime<Utc>,
+}
+
+impl TimeRange {
+ pub fn new(
+ starts_at: DateTime<Utc>,
+ ends_at: DateTime<Utc>,
+ ) -> Result<Self, DomainError> {
+ if starts_at >= ends_at {
+ Err(DomainError::InvalidTimeRange)
+ } else {
+ Ok(Self { starts_at, ends_at })
+ }
+ }
+
+ pub fn overlaps(&self, other: &Self) -> bool {
+ self.starts_at < other.ends_at
+ && other.starts_at < self.ends_at
+ }
+
+ pub fn duration(&self) -> chrono::Duration {
+ self.ends_at - self.starts_at
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Frequency {
+ Daily,
+ Weekly,
+ Monthly,
+ Yearly,
+}
+
+impl fmt::Display for Frequency {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Frequency::Daily => write!(f, "DAILY"),
+ Frequency::Weekly => write!(f, "WEEKLY"),
+ Frequency::Monthly => write!(f, "MONTHLY"),
+ Frequency::Yearly => write!(f, "YEARLY"),
+ }
+ }
+}
+
+impl std::str::FromStr for Frequency {
+ type Err = DomainError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s.to_uppercase().as_str() {
+ "DAILY" => Ok(Frequency::Daily),
+ "WEEKLY" => Ok(Frequency::Weekly),
+ "MONTHLY" => Ok(Frequency::Monthly),
+ "YEARLY" => Ok(Frequency::Yearly),
+ _ => Err(DomainError::InvalidFrequency),
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct CalendarId(Uuid);
+
+impl CalendarId {
+ pub fn new() -> Self {
+ Self(Uuid::new_v4())
+ }
+
+ pub fn from_uuid(uuid: Uuid) -> Self {
+ Self(uuid)
+ }
+
+ pub fn as_uuid(&self) -> Uuid {
+ self.0
+ }
+}
+
+impl std::fmt::Display for CalendarId {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.0)
+ }
+}
+
+impl std::str::FromStr for CalendarId {
+ type Err = uuid::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ Ok(Self(Uuid::parse_str(s)?))
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct EventId(Uuid);
+
+impl EventId {
+ pub fn new() -> Self {
+ Self(Uuid::new_v4())
+ }
+
+ pub fn from_uuid(uuid: Uuid) -> Self {
+ Self(uuid)
+ }
+
+ pub fn as_uuid(&self) -> Uuid {
+ self.0
+ }
+}
+
+impl std::fmt::Display for EventId {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.0)
+ }
+}
+
+impl std::str::FromStr for EventId {
+ type Err = uuid::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ Ok(Self(Uuid::parse_str(s)?))
+ }
+}