caldir_core/event/
to_icalendar.rs1use crate::event::{Availability, Event, Status};
2use icalendar::{Component, EventLike};
3
4impl From<&Event> for icalendar::Event {
5 fn from(value: &Event) -> Self {
6 let mut event = icalendar::Event::new();
7 event.starts(icalendar::DatePerhapsTime::from(&value.start));
8 event.uid(value.uid.as_str());
9
10 if let Some(end) = &value.end {
11 event.ends(icalendar::DatePerhapsTime::from(end));
12 }
13
14 if value.status != Status::default() {
19 event.append_property(icalendar::Property::new(
20 "STATUS",
21 value.status.as_ics_str(),
22 ));
23 }
24
25 if value.availability != Availability::default() {
26 event.append_property(icalendar::Property::new(
27 "TRANSP",
28 value.availability.as_ics_str(),
29 ));
30 }
31
32 if let Some(visibility) = value.visibility {
34 event.append_property(icalendar::Property::new("CLASS", visibility.as_ics_str()));
35 }
36
37 if let Some(recurrence) = &value.recurrence {
38 recurrence.apply_to(&mut event);
39 }
40
41 if let Some(recurrence_id) = &value.recurrence_id {
42 event.recurrence_id(icalendar::DatePerhapsTime::from(
43 recurrence_id.as_event_time(),
44 ));
45 }
46
47 if let Some(summary) = &value.summary {
48 event.summary(summary);
49 }
50
51 if let Some(description) = &value.description {
52 event.description(description);
53 }
54
55 if let Some(location) = &value.location {
56 event.location(location);
57 }
58
59 if let Some(last_modified) = value.last_modified {
60 event.last_modified(last_modified);
61 }
62
63 if value.sequence != 0 {
66 event.append_property(icalendar::Property::new(
67 "SEQUENCE",
68 value.sequence.to_string(),
69 ));
70 }
71
72 if let Some(organizer) = &value.organizer {
73 event.append_property(icalendar::Property::from(organizer));
74 }
75
76 for attendee in &value.attendees {
77 event.append_multi_property(icalendar::Property::from(attendee));
78 }
79
80 if let Some(url) = &value.url {
86 event.append_property(icalendar::Property::new("URL", url));
87 }
88
89 for attachment in &value.attachments {
90 event.append_multi_property(icalendar::Property::from(attachment));
91 }
92
93 for x in &value.x_properties {
94 event.append_property(icalendar::Property::from(x));
95 }
96
97 event.done()
98 }
99}
100
101impl From<Event> for icalendar::Event {
102 fn from(value: Event) -> Self {
103 icalendar::Event::from(&value)
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::EventTime;
111 use crate::event::{EventUid, RecurrenceId, Visibility};
112 use crate::test_utils::test_event;
113
114 #[test]
115 fn converts_summary() {
116 let mut event = test_event();
117 event.summary = Some("Hello world".to_string());
118
119 let ical_event: icalendar::Event = event.into();
120
121 assert_eq!(ical_event.get_summary(), Some("Hello world"));
122 }
123
124 #[test]
125 fn converts_location() {
126 let mut event = test_event();
127 event.location = Some("New York".to_string());
128
129 let ical_event: icalendar::Event = event.into();
130
131 assert_eq!(ical_event.get_location(), Some("New York"));
132 }
133
134 #[test]
135 fn converts_uid() {
136 let mut event = test_event();
137 event.uid = EventUid::new("abc123@hooli.com".to_string());
138
139 let ical_event: icalendar::Event = event.into();
140
141 assert_eq!(ical_event.get_uid(), Some("abc123@hooli.com"));
142 }
143
144 #[test]
145 fn converts_description() {
146 let mut event = test_event();
147 event.description = Some("Multi-line\nnotes".to_string());
148
149 let ical_event: icalendar::Event = event.into();
150
151 assert_eq!(ical_event.get_description(), Some("Multi-line\nnotes"));
152 }
153
154 #[test]
155 fn converts_start() {
156 let mut event = test_event();
157 event.start = EventTime::Date(chrono::NaiveDate::from_ymd_opt(2026, 10, 10).unwrap());
158
159 let ical_event: icalendar::Event = event.into();
160
161 assert_eq!(
162 ical_event.get_start(),
163 Some(icalendar::DatePerhapsTime::Date(
164 chrono::NaiveDate::from_ymd_opt(2026, 10, 10).unwrap()
165 ))
166 );
167 }
168
169 #[test]
170 fn converts_end() {
171 let mut event = test_event();
172 event.end = Some(EventTime::Date(
173 chrono::NaiveDate::from_ymd_opt(2026, 10, 11).unwrap(),
174 ));
175
176 let ical_event: icalendar::Event = event.into();
177
178 assert_eq!(
179 ical_event.get_end(),
180 Some(icalendar::DatePerhapsTime::Date(
181 chrono::NaiveDate::from_ymd_opt(2026, 10, 11).unwrap()
182 ))
183 );
184 }
185
186 #[test]
187 fn omits_end_when_none() {
188 let mut event = test_event();
189 event.end = None;
190
191 let ical_event: icalendar::Event = event.into();
192
193 assert_eq!(ical_event.get_end(), None);
194 }
195
196 #[test]
197 fn converts_status() {
198 let mut event = test_event();
199 event.status = Status::Cancelled;
200
201 let ical_event: icalendar::Event = event.into();
202
203 assert_eq!(ical_event.property_value("STATUS"), Some("CANCELLED"));
204 }
205
206 #[test]
207 fn omits_status_when_default() {
208 let mut event = test_event();
209 event.status = Status::Confirmed;
210
211 let ical_event: icalendar::Event = event.into();
212
213 assert_eq!(ical_event.property_value("STATUS"), None);
214 }
215
216 #[test]
217 fn converts_availability() {
218 let mut event = test_event();
219 event.availability = Availability::Free;
220
221 let ical_event: icalendar::Event = event.into();
222
223 assert_eq!(ical_event.property_value("TRANSP"), Some("TRANSPARENT"));
224 }
225
226 #[test]
227 fn omits_availability_when_default() {
228 let mut event = test_event();
229 event.availability = Availability::Busy;
230
231 let ical_event: icalendar::Event = event.into();
232
233 assert_eq!(ical_event.property_value("TRANSP"), None);
234 }
235
236 #[test]
237 fn converts_visibility() {
238 let mut event = test_event();
239 event.visibility = Some(Visibility::Private);
240
241 let ical_event: icalendar::Event = event.into();
242
243 assert_eq!(ical_event.property_value("CLASS"), Some("PRIVATE"));
244 }
245
246 #[test]
247 fn omits_visibility_when_unspecified() {
248 let mut event = test_event();
249 event.visibility = None;
250
251 let ical_event: icalendar::Event = event.into();
252
253 assert_eq!(ical_event.property_value("CLASS"), None);
254 }
255
256 #[test]
257 fn writes_explicit_public_visibility() {
258 let mut event = test_event();
261 event.visibility = Some(Visibility::Public);
262
263 let ical_event: icalendar::Event = event.into();
264
265 assert_eq!(ical_event.property_value("CLASS"), Some("PUBLIC"));
266 }
267
268 #[test]
269 fn converts_recurrence() {
270 let mut event = test_event();
271 event.recurrence = Some(crate::event::Recurrence::new("FREQ=WEEKLY;BYDAY=MO"));
272
273 let ical_event: icalendar::Event = event.into();
274
275 assert_eq!(
276 ical_event.property_value("RRULE"),
277 Some("FREQ=WEEKLY;BYDAY=MO")
278 );
279 }
280
281 #[test]
282 fn omits_recurrence_when_none() {
283 let mut event = test_event();
284 event.recurrence = None;
285
286 let ical_event: icalendar::Event = event.into();
287
288 assert_eq!(ical_event.property_value("RRULE"), None);
289 }
290
291 #[test]
292 fn converts_recurrence_id() {
293 let mut event = test_event();
294 event.recurrence_id = Some(RecurrenceId::from_event_time(EventTime::Date(
295 chrono::NaiveDate::from_ymd_opt(2026, 5, 15).unwrap(),
296 )));
297
298 let ical_event: icalendar::Event = event.into();
299
300 assert_eq!(
301 ical_event.get_recurrence_id(),
302 Some(icalendar::DatePerhapsTime::Date(
303 chrono::NaiveDate::from_ymd_opt(2026, 5, 15).unwrap()
304 ))
305 );
306 }
307
308 #[test]
309 fn omits_recurrence_id_when_none() {
310 let mut event = test_event();
311 event.recurrence_id = None;
312
313 let ical_event: icalendar::Event = event.into();
314
315 assert_eq!(ical_event.get_recurrence_id(), None);
316 }
317
318 #[test]
319 fn converts_last_modified() {
320 let last_modified = chrono::NaiveDate::from_ymd_opt(2026, 5, 2)
321 .unwrap()
322 .and_hms_opt(17, 39, 14)
323 .unwrap()
324 .and_utc();
325
326 let mut event = test_event();
327 event.last_modified = Some(last_modified);
328
329 let ical_event: icalendar::Event = event.into();
330
331 assert_eq!(
332 ical_event.property_value("LAST-MODIFIED"),
333 Some("20260502T173914Z")
334 );
335 }
336
337 #[test]
338 fn omits_last_modified_when_none() {
339 let mut event = test_event();
340 event.last_modified = None;
341
342 let ical_event: icalendar::Event = event.into();
343
344 assert_eq!(ical_event.property_value("LAST-MODIFIED"), None);
345 }
346
347 #[test]
348 fn converts_sequence() {
349 let mut event = test_event();
350 event.sequence = 3;
351
352 let ical_event: icalendar::Event = event.into();
353
354 assert_eq!(ical_event.property_value("SEQUENCE"), Some("3"));
355 }
356
357 #[test]
358 fn converts_negative_sequence() {
359 let mut event = test_event();
360 event.sequence = -1;
361
362 let ical_event: icalendar::Event = event.into();
363
364 assert_eq!(ical_event.property_value("SEQUENCE"), Some("-1"));
365 }
366
367 #[test]
368 fn omits_sequence_when_default() {
369 let mut event = test_event();
370 event.sequence = 0;
371
372 let ical_event: icalendar::Event = event.into();
373
374 assert_eq!(ical_event.property_value("SEQUENCE"), None);
375 }
376
377 #[test]
378 fn converts_organizer() {
379 let mut event = test_event();
380 event.organizer = Some(crate::event::Organizer::new("alice@example.com"));
381
382 let ical_event: icalendar::Event = event.into();
383
384 assert_eq!(
385 ical_event.property_value("ORGANIZER"),
386 Some("mailto:alice@example.com")
387 );
388 }
389
390 #[test]
391 fn omits_organizer_when_none() {
392 let mut event = test_event();
393 event.organizer = None;
394
395 let ical_event: icalendar::Event = event.into();
396
397 assert!(ical_event.properties().get("ORGANIZER").is_none());
398 }
399
400 #[test]
401 fn converts_attendees() {
402 let mut event = test_event();
403 event.attendees = vec![
404 crate::event::Attendee::new("bob@example.com"),
405 crate::event::Attendee::new("carol@example.com"),
406 ];
407
408 let ical_event: icalendar::Event = event.into();
409
410 let attendees = ical_event
411 .multi_properties()
412 .get("ATTENDEE")
413 .expect("ATTENDEE multi-property should be present");
414 assert_eq!(
415 attendees.iter().map(|p| p.value()).collect::<Vec<_>>(),
416 vec!["mailto:bob@example.com", "mailto:carol@example.com"]
417 );
418 }
419
420 #[test]
421 fn omits_attendees_when_empty() {
422 let mut event = test_event();
423 event.attendees = vec![];
424
425 let ical_event: icalendar::Event = event.into();
426
427 assert!(ical_event.multi_properties().get("ATTENDEE").is_none());
428 }
429
430 #[test]
435 fn converts_url() {
436 let mut event = test_event();
437 event.url = Some("https://meet.example.com/abc-defg-hij".to_string());
438
439 let ical_event: icalendar::Event = event.into();
440
441 assert_eq!(
442 ical_event.property_value("URL"),
443 Some("https://meet.example.com/abc-defg-hij")
444 );
445 }
446
447 #[test]
448 fn omits_url_when_none() {
449 let mut event = test_event();
450 event.url = None;
451
452 let ical_event: icalendar::Event = event.into();
453
454 assert_eq!(ical_event.property_value("URL"), None);
455 }
456
457 #[test]
458 fn converts_attachments() {
459 let mut event = test_event();
460 event.attachments = vec![
461 crate::event::Attachment::new("https://drive.google.com/file/d/abc"),
462 crate::event::Attachment::new("https://example.com/report.doc"),
463 ];
464
465 let ical_event: icalendar::Event = event.into();
466
467 let attachments = ical_event
468 .multi_properties()
469 .get("ATTACH")
470 .expect("ATTACH multi-property should be present");
471 assert_eq!(
472 attachments.iter().map(|p| p.value()).collect::<Vec<_>>(),
473 vec![
474 "https://drive.google.com/file/d/abc",
475 "https://example.com/report.doc"
476 ]
477 );
478 }
479
480 #[test]
481 fn omits_attachments_when_empty() {
482 let mut event = test_event();
483 event.attachments = vec![];
484
485 let ical_event: icalendar::Event = event.into();
486
487 assert!(ical_event.multi_properties().get("ATTACH").is_none());
488 }
489
490 #[test]
491 fn converts_x_properties() {
492 let mut event = test_event();
493 event.x_properties = vec![crate::event::XProperty::new(
494 "X-HOOLI-EVENT-ID",
495 "abc123@hooli.com",
496 )];
497
498 let ical_event: icalendar::Event = event.into();
499
500 assert_eq!(
501 ical_event.property_value("X-HOOLI-EVENT-ID"),
502 Some("abc123@hooli.com")
503 );
504 }
505
506 #[test]
507 fn omits_x_properties_when_empty() {
508 let mut event = test_event();
509 event.x_properties = vec![];
510
511 let ical_event: icalendar::Event = event.into();
512
513 assert!(ical_event.properties().keys().all(|k| !k.starts_with("X-")));
514 }
515}