1use chrono::{DateTime, NaiveDate, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::message::BodyContentType;
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9pub struct Event {
10 pub account: String,
11 pub calendar_id: String,
12 pub id: String,
13 pub subject: String,
14 pub start: EventTime,
15 pub end: EventTime,
16 #[serde(default)]
17 pub all_day: bool,
18 #[serde(default)]
19 pub location: Option<String>,
20 pub organizer: Attendee,
21 #[serde(default)]
22 pub attendees: Vec<Attendee>,
23 #[serde(default)]
24 pub body_preview: String,
25 #[serde(default)]
26 pub body_content: String,
27 #[serde(default)]
28 pub body_content_type: BodyContentType,
29 #[serde(default)]
30 pub recurrence: Option<RecurrencePattern>,
31 #[serde(default)]
32 pub is_organizer: bool,
33 #[serde(default)]
34 pub response_status: ResponseStatus,
35 #[serde(default)]
36 pub online_meeting_url: Option<String>,
37 #[serde(default)]
38 pub series_master_id: Option<String>,
39 #[serde(default)]
42 pub reminder_minutes: Option<u32>,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50pub struct EventTime {
51 pub at: DateTime<Utc>,
52 pub tz: String,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56pub struct Attendee {
57 pub name: String,
58 pub address: String,
59 #[serde(default)]
60 pub kind: AttendeeKind,
61 #[serde(default)]
62 pub response: ResponseStatus,
63}
64
65#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
66#[serde(rename_all = "camelCase")]
67pub enum AttendeeKind {
68 #[default]
69 Required,
70 Optional,
71 Resource,
72}
73
74#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(rename_all = "camelCase")]
76pub enum ResponseStatus {
77 #[default]
78 None,
79 Organizer,
80 Accepted,
81 Tentative,
82 Declined,
83 NotResponded,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87pub struct RecurrencePattern {
88 pub freq: RecurrenceFreq,
89 pub interval: u32,
91 #[serde(default)]
93 pub by_weekday: Vec<Weekday>,
94 pub range: RecurrenceRange,
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub enum RecurrenceFreq {
100 Daily,
101 Weekly,
102 Monthly,
103 Yearly,
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
107#[serde(rename_all = "camelCase")]
108pub enum Weekday {
109 Monday,
110 Tuesday,
111 Wednesday,
112 Thursday,
113 Friday,
114 Saturday,
115 Sunday,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119#[serde(tag = "kind", content = "value", rename_all = "camelCase")]
120pub enum RecurrenceRange {
121 EndDate(NaiveDate),
122 Count(u32),
123 NoEnd,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127pub struct Calendar {
128 pub account: String,
129 pub id: String,
130 pub name: String,
131 #[serde(default)]
132 pub is_default: bool,
133 #[serde(default)]
134 pub color: Option<String>,
135 #[serde(default)]
136 pub can_edit: bool,
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn event_roundtrips_through_json() {
145 let e = Event {
146 account: "u@e.com".into(),
147 calendar_id: "cal-1".into(),
148 id: "evt-1".into(),
149 subject: "Team sync".into(),
150 start: EventTime {
151 at: chrono::DateTime::parse_from_rfc3339("2026-05-22T13:00:00Z")
152 .unwrap()
153 .to_utc(),
154 tz: "Europe/Stockholm".into(),
155 },
156 end: EventTime {
157 at: chrono::DateTime::parse_from_rfc3339("2026-05-22T14:00:00Z")
158 .unwrap()
159 .to_utc(),
160 tz: "Europe/Stockholm".into(),
161 },
162 all_day: false,
163 location: Some("Office".into()),
164 organizer: Attendee {
165 name: "Kristofer".into(),
166 address: "k@mklab.se".into(),
167 kind: AttendeeKind::Required,
168 response: ResponseStatus::Organizer,
169 },
170 attendees: vec![],
171 body_preview: "Weekly".into(),
172 body_content: "Weekly sync".into(),
173 body_content_type: BodyContentType::Text,
174 recurrence: Some(RecurrencePattern {
175 freq: RecurrenceFreq::Weekly,
176 interval: 1,
177 by_weekday: vec![Weekday::Monday],
178 range: RecurrenceRange::NoEnd,
179 }),
180 is_organizer: true,
181 response_status: ResponseStatus::Organizer,
182 online_meeting_url: None,
183 series_master_id: None,
184 reminder_minutes: Some(15),
185 };
186 let json = serde_json::to_string(&e).unwrap();
187 let e2: Event = serde_json::from_str(&json).unwrap();
188 assert_eq!(e, e2);
189 }
190
191 #[test]
192 fn recurrence_range_roundtrips_tagged() {
193 for r in [
194 RecurrenceRange::NoEnd,
195 RecurrenceRange::Count(10),
196 RecurrenceRange::EndDate(NaiveDate::from_ymd_opt(2026, 12, 31).unwrap()),
197 ] {
198 let j = serde_json::to_string(&r).unwrap();
199 let r2: RecurrenceRange = serde_json::from_str(&j).unwrap();
200 assert_eq!(r, r2);
201 }
202 }
203
204 #[test]
205 fn response_status_serializes_camel_case() {
206 assert_eq!(
207 serde_json::to_string(&ResponseStatus::NotResponded).unwrap(),
208 "\"notResponded\""
209 );
210 }
211}