aimcal_core/
event.rs

1// SPDX-FileCopyrightText: 2025 Zexin Yuan <aim@yzx9.xyz>
2//
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{fmt::Display, num::NonZeroU32, str::FromStr};
6
7use chrono::{DateTime, Local};
8use icalendar::{Component, EventLike};
9
10use crate::LooseDateTime;
11
12/// Trait representing a calendar event.
13pub trait Event {
14    /// The short identifier for the event.
15    /// It will be `None` if the event does not have a short ID.
16    /// It is used for display purposes and may not be unique.
17    fn short_id(&self) -> Option<NonZeroU32> {
18        None
19    }
20
21    /// The unique identifier for the event.
22    fn uid(&self) -> &str;
23
24    /// The description of the event, if available.
25    fn description(&self) -> Option<&str>;
26
27    /// The location of the event, if available.
28    fn start(&self) -> Option<LooseDateTime>;
29
30    /// The start date and time of the event, if available.
31    fn end(&self) -> Option<LooseDateTime>;
32
33    /// The status of the event, if available.
34    fn status(&self) -> Option<EventStatus>;
35
36    /// The summary of the event.
37    fn summary(&self) -> &str;
38}
39
40impl Event for icalendar::Event {
41    fn uid(&self) -> &str {
42        self.get_uid().unwrap_or("")
43    }
44
45    fn description(&self) -> Option<&str> {
46        self.get_description()
47    }
48
49    fn start(&self) -> Option<LooseDateTime> {
50        self.get_start().map(Into::into)
51    }
52
53    fn end(&self) -> Option<LooseDateTime> {
54        self.get_end().map(Into::into)
55    }
56
57    fn status(&self) -> Option<EventStatus> {
58        self.get_status().map(EventStatus::from)
59    }
60
61    fn summary(&self) -> &str {
62        self.get_summary().unwrap_or("")
63    }
64}
65
66/// Darft for an event, used for creating new events.
67#[derive(Debug)]
68pub struct EventDraft {
69    /// The description of the event, if available.
70    pub description: Option<String>,
71
72    /// The start date and time of the event, if available.
73    pub start: Option<LooseDateTime>,
74
75    /// The end date and time of the event, if available.
76    pub end: Option<LooseDateTime>,
77
78    /// The status of the event.
79    pub status: EventStatus,
80
81    /// The summary of the event.
82    pub summary: String,
83}
84
85impl EventDraft {
86    /// Creates a new empty patch.
87    pub(crate) fn default() -> Self {
88        Self {
89            description: None,
90            start: None,
91            end: None,
92            status: EventStatus::default(),
93            summary: String::new(),
94        }
95    }
96
97    /// Converts the draft into a icalendar Event component.
98    pub(crate) fn into_ics(self, uid: &str) -> icalendar::Event {
99        let mut event = icalendar::Event::with_uid(uid);
100
101        if let Some(description) = self.description {
102            Component::description(&mut event, &description);
103        }
104
105        if let Some(start) = self.start {
106            EventLike::starts(&mut event, start);
107        }
108
109        if let Some(end) = self.end {
110            EventLike::ends(&mut event, end);
111        }
112
113        icalendar::Event::status(&mut event, self.status.into());
114
115        Component::summary(&mut event, &self.summary);
116
117        event
118    }
119}
120
121/// Patch for an event, allowing partial updates.
122#[derive(Debug, Default, Clone)]
123pub struct EventPatch {
124    /// The description of the event, if available.
125    pub description: Option<Option<String>>,
126
127    /// The start date and time of the event, if available.
128    pub start: Option<Option<LooseDateTime>>,
129
130    /// The end date and time of the event, if available.
131    pub end: Option<Option<LooseDateTime>>,
132
133    /// The status of the event, if available.
134    pub status: Option<EventStatus>,
135
136    /// The summary of the event, if available.
137    pub summary: Option<String>,
138}
139
140impl EventPatch {
141    /// Is this patch empty, meaning no fields are set
142    pub fn is_empty(&self) -> bool {
143        self.description.is_none()
144            && self.start.is_none()
145            && self.end.is_none()
146            && self.status.is_none()
147            && self.summary.is_none()
148    }
149
150    /// Applies the patch to a mutable event, modifying it in place.
151    pub(crate) fn apply_to<'a>(&self, e: &'a mut icalendar::Event) -> &'a mut icalendar::Event {
152        if let Some(description) = &self.description {
153            match description {
154                Some(desc) => e.description(desc),
155                None => e.remove_description(),
156            };
157        }
158
159        if let Some(start) = &self.start {
160            match start {
161                Some(s) => e.starts(*s),
162                None => e.remove_starts(),
163            };
164        }
165
166        if let Some(end) = &self.end {
167            match end {
168                Some(ed) => e.ends(*ed),
169                None => e.remove_ends(),
170            };
171        }
172
173        if let Some(status) = self.status {
174            e.status(status.into());
175        }
176
177        if let Some(summary) = &self.summary {
178            e.summary(summary);
179        }
180
181        e
182    }
183}
184
185/// The status of an event, which can be tentative, confirmed, or cancelled.
186#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
187#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
188pub enum EventStatus {
189    /// The event is tentative.
190    Tentative,
191
192    /// The event is confirmed.
193    #[default]
194    Confirmed,
195
196    /// The event is cancelled.
197    Cancelled,
198}
199
200const STATUS_TENTATIVE: &str = "TENTATIVE";
201const STATUS_CONFIRMED: &str = "CONFIRMED";
202const STATUS_CANCELLED: &str = "CANCELLED";
203
204impl AsRef<str> for EventStatus {
205    fn as_ref(&self) -> &str {
206        match self {
207            EventStatus::Tentative => STATUS_TENTATIVE,
208            EventStatus::Confirmed => STATUS_CONFIRMED,
209            EventStatus::Cancelled => STATUS_CANCELLED,
210        }
211    }
212}
213
214impl Display for EventStatus {
215    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216        write!(f, "{}", self.as_ref())
217    }
218}
219
220impl FromStr for EventStatus {
221    type Err = ();
222
223    fn from_str(value: &str) -> Result<Self, Self::Err> {
224        match value {
225            STATUS_TENTATIVE => Ok(EventStatus::Tentative),
226            STATUS_CONFIRMED => Ok(EventStatus::Confirmed),
227            STATUS_CANCELLED => Ok(EventStatus::Cancelled),
228            _ => Err(()),
229        }
230    }
231}
232
233impl From<EventStatus> for icalendar::EventStatus {
234    fn from(status: EventStatus) -> Self {
235        match status {
236            EventStatus::Tentative => icalendar::EventStatus::Tentative,
237            EventStatus::Confirmed => icalendar::EventStatus::Confirmed,
238            EventStatus::Cancelled => icalendar::EventStatus::Cancelled,
239        }
240    }
241}
242
243impl From<icalendar::EventStatus> for EventStatus {
244    fn from(status: icalendar::EventStatus) -> Self {
245        match status {
246            icalendar::EventStatus::Tentative => EventStatus::Tentative,
247            icalendar::EventStatus::Confirmed => EventStatus::Confirmed,
248            icalendar::EventStatus::Cancelled => EventStatus::Cancelled,
249        }
250    }
251}
252
253/// Conditions for filtering events in a calendar.
254#[derive(Debug, Clone, Copy)]
255pub struct EventConditions {
256    /// Whether to include only startable events.
257    pub startable: bool,
258}
259
260#[derive(Debug)]
261pub(crate) struct ParsedEventConditions {
262    /// The date and time after which the event must end to be considered startable.
263    pub end_after: Option<DateTime<Local>>,
264}
265
266impl ParsedEventConditions {
267    pub fn parse(now: &DateTime<Local>, conds: &EventConditions) -> Self {
268        Self {
269            end_after: conds.startable.then_some(*now),
270        }
271    }
272}