1use std::{fmt::Display, num::NonZeroU32, str::FromStr};
6
7use chrono::{DateTime, Duration, Local};
8use icalendar::{Component, EventLike};
9
10use crate::{DateTimeAnchor, LooseDateTime};
11
12pub trait Event {
14 fn short_id(&self) -> Option<NonZeroU32> {
18 None
19 }
20
21 fn uid(&self) -> &str;
23
24 fn description(&self) -> Option<&str>;
26
27 fn start(&self) -> Option<LooseDateTime>;
29
30 fn end(&self) -> Option<LooseDateTime>;
32
33 fn status(&self) -> Option<EventStatus>;
35
36 fn summary(&self) -> &str;
38}
39
40impl Event for icalendar::Event {
41 fn uid(&self) -> &str {
42 self.get_uid().unwrap_or_default()
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_default()
63 }
64}
65
66#[derive(Debug)]
68pub struct EventDraft {
69 pub description: Option<String>,
71
72 pub start: Option<LooseDateTime>,
74
75 pub end: Option<LooseDateTime>,
77
78 pub status: EventStatus,
80
81 pub summary: String,
83}
84
85impl EventDraft {
86 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 pub(crate) fn into_ics(self, now: &DateTime<Local>, 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 let default_duration = Duration::hours(1);
106 let (start, end) = match (self.start, self.end) {
107 (Some(start), Some(end)) => (start, end),
108 (None, Some(end)) => {
109 let start = match end {
111 LooseDateTime::DateOnly(d) => d.into(),
112 LooseDateTime::Floating(dt) => (dt - default_duration).into(),
113 LooseDateTime::Local(dt) => (dt - default_duration).into(),
114 };
115 (start, end)
116 }
117 (Some(start), None) => {
118 let end = match start {
120 LooseDateTime::DateOnly(d) => d.into(),
121 LooseDateTime::Floating(dt) => (dt + default_duration).into(),
122 LooseDateTime::Local(dt) => (dt + default_duration).into(),
123 };
124 (start, end)
125 }
126 (None, None) => {
127 let start = *now;
128 let end = (start + default_duration).into();
129 (start.into(), end)
130 }
131 };
132 EventLike::starts(&mut event, start);
133 EventLike::ends(&mut event, end);
134
135 icalendar::Event::status(&mut event, self.status.into());
136
137 Component::summary(&mut event, &self.summary);
138
139 event
140 }
141}
142
143#[derive(Debug, Default, Clone)]
145pub struct EventPatch {
146 pub description: Option<Option<String>>,
148
149 pub start: Option<Option<LooseDateTime>>,
151
152 pub end: Option<Option<LooseDateTime>>,
154
155 pub status: Option<EventStatus>,
157
158 pub summary: Option<String>,
160}
161
162impl EventPatch {
163 pub fn is_empty(&self) -> bool {
165 self.description.is_none()
166 && self.start.is_none()
167 && self.end.is_none()
168 && self.status.is_none()
169 && self.summary.is_none()
170 }
171
172 pub(crate) fn apply_to<'a>(&self, e: &'a mut icalendar::Event) -> &'a mut icalendar::Event {
174 if let Some(description) = &self.description {
175 match description {
176 Some(desc) => e.description(desc),
177 None => e.remove_description(),
178 };
179 }
180
181 if let Some(start) = &self.start {
182 match start {
183 Some(s) => e.starts(*s),
184 None => e.remove_starts(),
185 };
186 }
187
188 if let Some(end) = &self.end {
189 match end {
190 Some(ed) => e.ends(*ed),
191 None => e.remove_ends(),
192 };
193 }
194
195 if let Some(status) = self.status {
196 e.status(status.into());
197 }
198
199 if let Some(summary) = &self.summary {
200 e.summary(summary);
201 }
202
203 e
204 }
205}
206
207#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
209#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
210pub enum EventStatus {
211 Tentative,
213
214 #[default]
216 Confirmed,
217
218 Cancelled,
220}
221
222const STATUS_TENTATIVE: &str = "TENTATIVE";
223const STATUS_CONFIRMED: &str = "CONFIRMED";
224const STATUS_CANCELLED: &str = "CANCELLED";
225
226impl AsRef<str> for EventStatus {
227 fn as_ref(&self) -> &str {
228 match self {
229 EventStatus::Tentative => STATUS_TENTATIVE,
230 EventStatus::Confirmed => STATUS_CONFIRMED,
231 EventStatus::Cancelled => STATUS_CANCELLED,
232 }
233 }
234}
235
236impl Display for EventStatus {
237 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238 write!(f, "{}", self.as_ref())
239 }
240}
241
242impl FromStr for EventStatus {
243 type Err = ();
244
245 fn from_str(value: &str) -> Result<Self, Self::Err> {
246 match value {
247 STATUS_TENTATIVE => Ok(EventStatus::Tentative),
248 STATUS_CONFIRMED => Ok(EventStatus::Confirmed),
249 STATUS_CANCELLED => Ok(EventStatus::Cancelled),
250 _ => Err(()),
251 }
252 }
253}
254
255impl From<EventStatus> for icalendar::EventStatus {
256 fn from(status: EventStatus) -> Self {
257 match status {
258 EventStatus::Tentative => icalendar::EventStatus::Tentative,
259 EventStatus::Confirmed => icalendar::EventStatus::Confirmed,
260 EventStatus::Cancelled => icalendar::EventStatus::Cancelled,
261 }
262 }
263}
264
265impl From<icalendar::EventStatus> for EventStatus {
266 fn from(status: icalendar::EventStatus) -> Self {
267 match status {
268 icalendar::EventStatus::Tentative => EventStatus::Tentative,
269 icalendar::EventStatus::Confirmed => EventStatus::Confirmed,
270 icalendar::EventStatus::Cancelled => EventStatus::Cancelled,
271 }
272 }
273}
274
275#[derive(Debug, Default, Clone, Copy)]
277pub struct EventConditions {
278 pub startable: Option<DateTimeAnchor>,
280
281 pub cutoff: Option<DateTimeAnchor>,
283}
284
285#[derive(Debug)]
286pub(crate) struct ParsedEventConditions {
287 pub start_before: Option<DateTime<Local>>,
289
290 pub end_after: Option<DateTime<Local>>,
292}
293
294impl ParsedEventConditions {
295 pub fn parse(now: &DateTime<Local>, conds: &EventConditions) -> Self {
296 Self {
297 start_before: conds.cutoff.map(|w| w.parse_as_end_of_day(now)),
298 end_after: conds.startable.map(|w| w.parse_as_start_of_day(now)),
299 }
300 }
301}