1use std::{fmt::Display, num::NonZeroU32, str::FromStr};
6
7use chrono::{DateTime, Duration, Local, Timelike, Utc};
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, Clone)]
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(now: &DateTime<Local>) -> Self {
88 let start = if now.minute() < 30 {
90 now.with_minute(30).unwrap().with_second(0).unwrap()
91 } else {
92 (*now + Duration::hours(1))
93 .with_minute(0)
94 .unwrap()
95 .with_second(0)
96 .unwrap()
97 };
98
99 Self {
100 description: None,
101 start: Some(start.into()),
102 end: Some((start + Duration::hours(1)).into()),
103 status: EventStatus::default(),
104 summary: String::new(),
105 }
106 }
107
108 pub(crate) fn resolve<'a>(&'a self, now: &'a DateTime<Local>) -> ResolvedEventDraft<'a> {
109 let default_duration = Duration::hours(1);
110 let (start, end) = match (self.start, self.end) {
111 (Some(start), Some(end)) => (start, end),
112 (None, Some(end)) => {
113 let start = match end {
115 LooseDateTime::DateOnly(d) => d.into(),
116 LooseDateTime::Floating(dt) => (dt - default_duration).into(),
117 LooseDateTime::Local(dt) => (dt - default_duration).into(),
118 };
119 (start, end)
120 }
121 (Some(start), None) => {
122 let end = match start {
124 LooseDateTime::DateOnly(d) => d.into(),
125 LooseDateTime::Floating(dt) => (dt + default_duration).into(),
126 LooseDateTime::Local(dt) => (dt + default_duration).into(),
127 };
128 (start, end)
129 }
130 (None, None) => {
131 let start = *now;
132 let end = (start + default_duration).into();
133 (start.into(), end)
134 }
135 };
136
137 ResolvedEventDraft {
138 description: self.description.as_deref(),
139 start,
140 end,
141 status: self.status,
142 summary: &self.summary,
143
144 now,
145 }
146 }
147}
148
149#[derive(Debug, Clone, Copy)]
150pub struct ResolvedEventDraft<'a> {
151 pub description: Option<&'a str>,
152 pub start: LooseDateTime,
153 pub end: LooseDateTime,
154 pub status: EventStatus,
155 pub summary: &'a str,
156
157 pub now: &'a DateTime<Local>,
158}
159
160impl ResolvedEventDraft<'_> {
161 pub(crate) fn into_ics(self, uid: &str) -> icalendar::Event {
163 let mut event = icalendar::Event::with_uid(uid);
164
165 if let Some(description) = self.description {
166 Component::description(&mut event, description);
167 }
168
169 EventLike::starts(&mut event, self.start);
170 EventLike::ends(&mut event, self.end);
171
172 icalendar::Event::status(&mut event, self.status.into());
173
174 Component::summary(&mut event, self.summary);
175
176 Component::created(&mut event, self.now.with_timezone(&Utc));
178 event
179 }
180}
181
182#[derive(Debug, Default, Clone)]
184pub struct EventPatch {
185 pub description: Option<Option<String>>,
187
188 pub start: Option<Option<LooseDateTime>>,
190
191 pub end: Option<Option<LooseDateTime>>,
193
194 pub status: Option<EventStatus>,
196
197 pub summary: Option<String>,
199}
200
201impl EventPatch {
202 #[must_use]
204 pub fn is_empty(&self) -> bool {
205 self.description.is_none()
206 && self.start.is_none()
207 && self.end.is_none()
208 && self.status.is_none()
209 && self.summary.is_none()
210 }
211
212 pub(crate) fn resolve(&self, now: DateTime<Local>) -> ResolvedEventPatch<'_> {
213 ResolvedEventPatch {
214 description: self.description.as_ref().map(|opt| opt.as_deref()),
215 start: self.start,
216 end: self.end,
217 status: self.status,
218 summary: self.summary.as_deref(),
219
220 now,
221 }
222 }
223}
224
225#[derive(Debug, Default, Clone, Copy)]
227pub struct ResolvedEventPatch<'a> {
228 pub description: Option<Option<&'a str>>,
229 pub start: Option<Option<LooseDateTime>>,
230 pub end: Option<Option<LooseDateTime>>,
231 pub status: Option<EventStatus>,
232 pub summary: Option<&'a str>,
233
234 pub now: DateTime<Local>,
235}
236
237impl ResolvedEventPatch<'_> {
238 pub fn apply_to<'b>(&self, e: &'b mut icalendar::Event) -> &'b mut icalendar::Event {
240 match self.description {
241 Some(Some(desc)) => e.description(desc),
242 Some(None) => e.remove_description(),
243 None => e,
244 };
245
246 match self.start {
247 Some(Some(start)) => e.starts(start),
248 Some(None) => e.remove_starts(),
249 None => e,
250 };
251
252 match self.end {
253 Some(Some(end)) => e.ends(end),
254 Some(None) => e.remove_ends(),
255 None => e,
256 };
257
258 if let Some(status) = self.status {
259 e.status(status.into());
260 }
261
262 if let Some(summary) = &self.summary {
263 e.summary(summary);
264 }
265
266 if e.get_created().is_none() {
268 Component::created(e, self.now.with_timezone(&Utc));
269 }
270 e
271 }
272}
273
274#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
276#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
277pub enum EventStatus {
278 Tentative,
280
281 #[default]
283 Confirmed,
284
285 Cancelled,
287}
288
289const STATUS_TENTATIVE: &str = "TENTATIVE";
290const STATUS_CONFIRMED: &str = "CONFIRMED";
291const STATUS_CANCELLED: &str = "CANCELLED";
292
293impl AsRef<str> for EventStatus {
294 fn as_ref(&self) -> &str {
295 match self {
296 EventStatus::Tentative => STATUS_TENTATIVE,
297 EventStatus::Confirmed => STATUS_CONFIRMED,
298 EventStatus::Cancelled => STATUS_CANCELLED,
299 }
300 }
301}
302
303impl Display for EventStatus {
304 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
305 self.as_ref().fmt(f)
306 }
307}
308
309impl FromStr for EventStatus {
310 type Err = ();
311
312 fn from_str(value: &str) -> Result<Self, Self::Err> {
313 match value {
314 STATUS_TENTATIVE => Ok(EventStatus::Tentative),
315 STATUS_CONFIRMED => Ok(EventStatus::Confirmed),
316 STATUS_CANCELLED => Ok(EventStatus::Cancelled),
317 _ => Err(()),
318 }
319 }
320}
321
322impl From<EventStatus> for icalendar::EventStatus {
323 fn from(status: EventStatus) -> Self {
324 match status {
325 EventStatus::Tentative => icalendar::EventStatus::Tentative,
326 EventStatus::Confirmed => icalendar::EventStatus::Confirmed,
327 EventStatus::Cancelled => icalendar::EventStatus::Cancelled,
328 }
329 }
330}
331
332impl From<icalendar::EventStatus> for EventStatus {
333 fn from(status: icalendar::EventStatus) -> Self {
334 match status {
335 icalendar::EventStatus::Tentative => EventStatus::Tentative,
336 icalendar::EventStatus::Confirmed => EventStatus::Confirmed,
337 icalendar::EventStatus::Cancelled => EventStatus::Cancelled,
338 }
339 }
340}
341
342#[derive(Debug, Default, Clone, Copy)]
344pub struct EventConditions {
345 pub startable: Option<DateTimeAnchor>,
347
348 pub cutoff: Option<DateTimeAnchor>,
350}
351
352impl EventConditions {
353 pub(crate) fn resolve(&self, now: &DateTime<Local>) -> ResolvedEventConditions {
354 ResolvedEventConditions {
355 start_before: self.cutoff.map(|w| w.resolve_at_end_of_day(now)),
356 end_after: self.startable.map(|w| w.resolve_at_start_of_day(now)),
357 }
358 }
359}
360
361#[derive(Debug, Clone, Copy)]
362pub struct ResolvedEventConditions {
363 pub start_before: Option<DateTime<Local>>,
365
366 pub end_after: Option<DateTime<Local>>,
368}