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