Skip to main content

icalendar/
components.rs

1#[cfg(feature = "recurrence")]
2use crate::recurrence::RecurrenceError;
3use chrono::{DateTime, NaiveDate, Utc};
4use uuid::Uuid;
5
6use std::{collections::BTreeMap, fmt, mem};
7
8use crate::{Attendee, properties::*};
9use date_time::{format_utc_date_time, naive_date_to_property, parse_utc_date_time};
10
11pub mod alarm;
12pub(crate) mod date_time;
13mod event;
14mod other;
15mod todo;
16mod venue;
17
18use alarm::*;
19use date_time::{CalendarDateTime, DatePerhapsTime};
20pub use event::*;
21pub use other::*;
22pub use todo::*;
23pub use venue::*;
24
25#[derive(Debug, Default, PartialEq, Eq, Clone)]
26pub(crate) struct InnerComponent {
27    pub properties: BTreeMap<String, Property>,
28    pub multi_properties: BTreeMap<String, Vec<Property>>,
29    pub components: Vec<Other>,
30}
31
32impl From<Other> for InnerComponent {
33    fn from(val: Other) -> Self {
34        val.inner
35    }
36}
37
38//impl<'a> Into<InnerComponent> for parser::Component<'a> {
39//    fn into(self) -> InnerComponent {
40//        unimplemented!()
41//    }
42//}
43
44impl InnerComponent {
45    /// End of builder pattern.
46    /// copies over everything
47    pub fn done(&mut self) -> Self {
48        InnerComponent {
49            properties: mem::take(&mut self.properties),
50            multi_properties: mem::take(&mut self.multi_properties),
51            components: mem::take(&mut self.components),
52        }
53    }
54
55    pub(crate) fn insert_multi(&mut self, property: impl Into<Property>) -> &mut Self {
56        let property = property.into();
57        let key = property.key().to_owned();
58
59        self.multi_properties
60            .entry(key)
61            .and_modify(|v| v.push(property.to_owned()))
62            .or_insert(vec![property.to_owned()]);
63        self
64    }
65
66    #[cfg(test)]
67    pub fn property_value(&self, key: &str) -> Option<&str> {
68        Some(self.properties.get(key)?.value())
69    }
70}
71
72/// Implemented by everything that goes into a `Calendar`
73pub trait Component {
74    /// Returns kind of component.
75    ///
76    ///
77    /// Must be ALL CAPS
78    /// These are used in the `BEGIN` and `END` line of the component.
79    fn component_kind(&self) -> String;
80
81    /// Allows access to the inner properties map.
82    fn properties(&self) -> &BTreeMap<String, Property>;
83
84    /// Allows access to the inner's child components.
85    fn components(&self) -> &[Other];
86
87    /// Read-only access to `multi_properties`
88    fn multi_properties(&self) -> &BTreeMap<String, Vec<Property>>;
89
90    /// Gets the value of a property.
91    fn property_value(&self, key: &str) -> Option<&str> {
92        Some(self.properties().get(key)?.value())
93    }
94
95    /// Writes [`Component`] using [`std::fmt`].
96    fn fmt_write<W: fmt::Write>(&self, out: &mut W) -> Result<(), fmt::Error> {
97        write_crlf!(out, "BEGIN:{}", self.component_kind())?;
98
99        if !self.properties().contains_key("DTSTAMP") {
100            let now = Utc::now();
101            write_crlf!(out, "DTSTAMP:{}", format_utc_date_time(now))?;
102        }
103
104        for property in self.properties().values() {
105            property.fmt_write(out)?;
106        }
107
108        if !self.properties().contains_key("UID") {
109            write_crlf!(out, "UID:{}", Uuid::new_v4())?;
110        }
111
112        for property in self.multi_properties().values().flatten() {
113            property.fmt_write(out)?;
114        }
115
116        for component in self.components() {
117            component.fmt_write(out)?;
118        }
119
120        write_crlf!(out, "END:{}", self.component_kind())?;
121        Ok(())
122    }
123
124    /// Serializes this component into [`rfc5545`](http://tools.ietf.org/html/rfc5545) again
125    ///
126    /// # Panic
127    /// this can panic if [`std::fmt::write`] returns an Error
128    /// use [`Component::try_into_string()`] if you don't like panicking
129    fn to_string(&self) -> String {
130        self.try_into_string().unwrap()
131    }
132
133    /// Serializes this component into [`rfc5545`](http://tools.ietf.org/html/rfc5545) again
134    fn try_into_string(&self) -> Result<String, fmt::Error> {
135        let mut out_string = String::new();
136        self.fmt_write(&mut out_string)?;
137        Ok(out_string)
138    }
139
140    /// Append a given [`Property`]
141    fn append_property(&mut self, property: impl Into<Property>) -> &mut Self;
142
143    /// Append a given [`Component`]
144    fn append_component(&mut self, child: impl Into<Other>) -> &mut Self;
145
146    /// Adds a [`Property`] of which there may be many
147    fn append_multi_property(&mut self, property: impl Into<Property>) -> &mut Self;
148
149    /// Construct and append a [`Property`]
150    fn add_property(&mut self, key: impl Into<String>, val: impl Into<String>) -> &mut Self {
151        self.append_property(Property::new(key, val))
152    }
153
154    /// Remove a property by its key
155    fn remove_property(&mut self, key: &str) -> &mut Self;
156
157    /// Remove a multi-property by its key
158    fn remove_multi_property(&mut self, key: &str) -> &mut Self;
159
160    #[deprecated]
161    /// Construct and append a [`Property`]
162    fn add_property_pre_alloc(&mut self, key: String, val: String) -> &mut Self {
163        self.append_property(Property::new(key, val))
164    }
165
166    /// Construct and append a [`Property`]
167    fn add_multi_property(&mut self, key: &str, val: &str) -> &mut Self {
168        self.append_multi_property(Property::new(key, val))
169    }
170
171    /// Set the [`DTSTAMP`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.7.2) [`Property`]
172    ///
173    /// This must be a UTC date-time value.
174    fn timestamp(&mut self, dt: DateTime<Utc>) -> &mut Self {
175        self.add_property("DTSTAMP", format_utc_date_time(dt))
176    }
177
178    /// Remove the [`DTSTAMP`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.7.2) [`Property`]
179    fn remove_timestamp(&mut self) -> &mut Self {
180        self.remove_property("DTSTAMP")
181    }
182
183    /// Gets the [`DTSTAMP`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.7.2) property.
184    fn get_timestamp(&self) -> Option<DateTime<Utc>> {
185        parse_utc_date_time(self.property_value("DTSTAMP")?)
186    }
187
188    /// Gets the [`DTSTART`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.2.4) [`Property`]
189    fn get_start(&self) -> Option<DatePerhapsTime> {
190        DatePerhapsTime::from_property(self.properties().get("DTSTART")?)
191    }
192
193    /// Gets the [`RECURRENCE-ID`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.4.4) property.
194    fn get_recurrence_id(&self) -> Option<DatePerhapsTime> {
195        DatePerhapsTime::from_property(self.properties().get("RECURRENCE-ID")?)
196    }
197
198    /// Gets the [`DTEND`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.2.2) [`Property`]
199    fn get_end(&self) -> Option<DatePerhapsTime> {
200        DatePerhapsTime::from_property(self.properties().get("DTEND")?)
201    }
202
203    /// Defines the relative priority.
204    ///
205    /// Ranges from 0 to 10, larger values will be truncated
206    fn priority(&mut self, priority: u32) -> &mut Self {
207        let priority = std::cmp::min(priority, 10);
208        self.add_property("PRIORITY", priority.to_string())
209    }
210
211    /// Removes the relative priority.
212    fn remove_priority(&mut self) -> &mut Self {
213        self.remove_property("PRIORITY")
214    }
215
216    // /// Add the [`ATTACH`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.1.1) property
217    // /// TODO: might have to move to Component
218    // pub fn attach(&mut self, attachment: ) -> &mut Self {
219    //     todo!()
220    //     // self.append_multi_property(todo!())
221    // }
222
223    /// Gets the relative priority.
224    ///
225    /// Ranges from 0 to 10.
226    fn get_priority(&self) -> Option<u32> {
227        let priority = self.property_value("PRIORITY")?.parse().ok()?;
228        if priority <= 10 { Some(priority) } else { None }
229    }
230
231    /// Prints to stdout
232    fn print(&self) -> Result<(), fmt::Error> {
233        let mut out = String::new();
234        self.fmt_write(&mut out)?;
235        print_crlf!("{}", out);
236        Ok(())
237    }
238
239    /// Set the summary
240    fn summary(&mut self, desc: &str) -> &mut Self {
241        self.add_property("SUMMARY", desc)
242    }
243
244    /// Removes the summary
245    fn remove_summary(&mut self) -> &mut Self {
246        self.remove_property("SUMMARY")
247    }
248
249    /// Gets the summary
250    fn get_summary(&self) -> Option<&str> {
251        self.property_value("SUMMARY")
252    }
253
254    /// Set the description
255    fn description(&mut self, desc: &str) -> &mut Self {
256        self.add_property("DESCRIPTION", desc)
257    }
258
259    /// Removes the description
260    fn remove_description(&mut self) -> &mut Self {
261        self.remove_property("DESCRIPTION")
262    }
263
264    /// Gets the description
265    fn get_description(&self) -> Option<&str> {
266        self.property_value("DESCRIPTION")
267    }
268
269    /// Adds an attendee
270    fn attendee(&mut self, attendee: Attendee) -> &mut Self {
271        self.append_multi_property(attendee)
272    }
273
274    /// Returns all `ATTENDEE` properties parsed as `Attendee` structs.
275    fn get_attendees(&self) -> Vec<Attendee> {
276        self.multi_properties()
277            .get("ATTENDEE")
278            .map(|v| {
279                v.iter()
280                    .filter_map(|p| Attendee::try_from(p).ok())
281                    .collect::<Vec<_>>()
282            })
283            .unwrap_or_default()
284    }
285
286    /// Set the UID
287    fn uid(&mut self, uid: &str) -> &mut Self {
288        self.add_property("UID", uid)
289    }
290
291    /// Gets the UID
292    fn get_uid(&self) -> Option<&str> {
293        self.property_value("UID")
294    }
295
296    /// Set the sequence
297    fn sequence(&mut self, sequence: u32) -> &mut Self {
298        self.add_property("SEQUENCE", sequence.to_string())
299    }
300
301    /// Removes the sequence
302    fn remove_sequence(&mut self) -> &mut Self {
303        self.remove_property("SEQUENCE")
304    }
305
306    /// Gets the sequence
307    fn get_sequence(&self) -> Option<u32> {
308        self.property_value("SEQUENCE").and_then(|s| s.parse().ok())
309    }
310
311    /// Set the visibility class
312    fn class(&mut self, class: Class) -> &mut Self {
313        self.append_property(class)
314    }
315
316    /// Removes the visibility class
317    fn remove_class(&mut self) -> &mut Self {
318        self.remove_property("CLASS")
319    }
320
321    /// Gets the visibility class
322    fn get_class(&self) -> Option<Class> {
323        Class::from_str(self.property_value("CLASS")?)
324    }
325
326    /// Sets the URL.
327    fn url(&mut self, url: &str) -> &mut Self {
328        self.add_property("URL", url)
329    }
330
331    /// Removes the URL.
332    fn remove_url(&mut self) -> &mut Self {
333        self.remove_property("URL")
334    }
335
336    /// Gets the URL.
337    fn get_url(&self) -> Option<&str> {
338        self.property_value("URL")
339    }
340
341    /// Set the [`LAST-MODIFIED`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.7.3) [`Property`]
342    ///
343    /// This must be a UTC date-time value.
344    fn last_modified(&mut self, dt: DateTime<Utc>) -> &mut Self {
345        self.add_property("LAST-MODIFIED", format_utc_date_time(dt))
346    }
347
348    /// Removes the [`LAST-MODIFIED`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.7.3) [`Property`]
349    fn remove_last_modified(&mut self) -> &mut Self {
350        self.remove_property("LAST-MODIFIED")
351    }
352
353    /// Gets the [`LAST-MODIFIED`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.7.3) property.
354    fn get_last_modified(&self) -> Option<DateTime<Utc>> {
355        parse_utc_date_time(self.property_value("LAST-MODIFIED")?)
356    }
357
358    /// Set the [`CREATED`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.7.1) [`Property`]
359    ///
360    /// This must be a UTC date-time value.
361    fn created(&mut self, dt: DateTime<Utc>) -> &mut Self {
362        self.add_property("CREATED", format_utc_date_time(dt))
363    }
364
365    /// Removes the [`CREATED`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.7.1) [`Property`]
366    fn remove_created(&mut self) -> &mut Self {
367        self.remove_property("CREATED")
368    }
369
370    /// Gets the [`CREATED`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.7.1) property.
371    fn get_created(&self) -> Option<DateTime<Utc>> {
372        parse_utc_date_time(self.property_value("CREATED")?)
373    }
374}
375
376/// Common trait of [`Event`] and [`Todo`]
377pub trait EventLike: Component {
378    /// Set the [`DTSTART`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.2.4) [`Property`]
379    ///
380    /// See [`DatePerhapsTime`] for info how are different [`chrono`] types converted automatically.
381    fn starts<T: Into<DatePerhapsTime>>(&mut self, dt: T) -> &mut Self {
382        let calendar_dt = dt.into();
383        self.append_property(calendar_dt.to_property("DTSTART"))
384    }
385
386    /// removes the [`DTSTART`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.2.4) [`Property`]
387    fn remove_starts(&mut self) -> &mut Self {
388        self.remove_property("DTSTART")
389    }
390
391    /// Set the [`DTEND`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.2.2) [`Property`]
392    ///
393    /// See [`DatePerhapsTime`] for info how are different [`chrono`] types converted automatically.
394    fn ends<T: Into<DatePerhapsTime>>(&mut self, dt: T) -> &mut Self {
395        let calendar_dt = dt.into();
396        self.append_property(calendar_dt.to_property("DTEND"))
397    }
398
399    /// Removes the [`DTEND`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.2.2) [`Property`]
400    fn remove_ends(&mut self) -> &mut Self {
401        self.remove_property("DTEND")
402    }
403
404    /// Sets the [`RECURRENCE-ID`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.4.4)
405    /// property.
406    ///
407    /// See [`DatePerhapsTime`] for info how are different [`chrono`] types converted automatically.
408    fn recurrence_id<T: Into<DatePerhapsTime>>(&mut self, dt: T) -> &mut Self {
409        let calendar_dt = dt.into();
410        self.append_property(calendar_dt.to_property("RECURRENCE-ID"))
411    }
412
413    /// Removes the [`RECURRENCE-ID`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.4.4)
414    fn remove_recurrence_id(&mut self) -> &mut Self {
415        self.remove_property("RECURRENCE-ID")
416    }
417
418    /// Set the [`DTSTART`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.2.4) [`Property`]
419    /// and [`DTEND`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.2.2) [`Property`],
420    /// date only
421    fn all_day(&mut self, date: NaiveDate) -> &mut Self {
422        self.append_property(naive_date_to_property(date, "DTSTART"))
423            .append_property(naive_date_to_property(date, "DTEND"))
424    }
425
426    /// Set the LOCATION with a VVENUE UID
427    /// iCalender venue draft
428    fn venue(&mut self, location: &str, venue_uid: &str) -> &mut Self {
429        self.append_property(
430            Property::new("LOCATION", location)
431                .append_parameter(Parameter::new("VVENUE", venue_uid))
432                .done(),
433        );
434        self
435    }
436
437    /// Set the LOCATION
438    /// 3.8.1.7.  Location
439    fn location(&mut self, location: &str) -> &mut Self {
440        self.add_property("LOCATION", location)
441    }
442
443    /// Removes the LOCATION with a VVENUE UID
444    fn remove_location(&mut self) -> &mut Self {
445        self.remove_property("LOCATION")
446    }
447
448    /// Gets the location
449    fn get_location(&self) -> Option<&str> {
450        self.property_value("LOCATION")
451    }
452
453    /// Set recurrence rules from an [`rrule::RRule`].
454    ///
455    /// The `DTSTART` of this component is used as the start date for the recurrence rule,
456    /// so `.starts()` or `.all_day()` must be called before `.recurrence()`.
457    ///
458    /// Returns `Err` if `DTSTART` is missing, the timezone name is unrecognised, or
459    /// the rule fails rrule's own validation.
460    #[cfg(feature = "recurrence")]
461    fn recurrence(&mut self, rrule: crate::UnvalidatedRRule) -> Result<&mut Self, RecurrenceError> {
462        // Derive dt_start from whatever DTSTART is already on the component.
463        let dt_start_prop = self
464            .properties()
465            .get("DTSTART")
466            .ok_or(RecurrenceError::MissingDtStart)?;
467
468        let dt_start = crate::recurrence::dt_start_to_rrule_datetime(dt_start_prop)?;
469
470        let rruleset = rrule.build(dt_start)?;
471
472        let rrule_str = rruleset
473            .get_rrule()
474            .iter()
475            .map(ToString::to_string)
476            .collect::<Vec<_>>()
477            .join("\n");
478
479        self.add_property("RRULE", rrule_str);
480        for dt in rruleset.get_rdate() {
481            self.rdate(CalendarDateTime::from(dt));
482        }
483        for dt in rruleset.get_exdate() {
484            self.rdate(CalendarDateTime::from(dt));
485        }
486
487        Ok(self)
488    }
489
490    // /// Get recurrence rules.
491    // ///
492    // /// Returns `None` if no `RRULE` property is present on this component, or if the
493    // /// rule could not be parsed. Use [`try_recurrence`](EventLike::try_recurrence) if
494    // /// you need to distinguish between the two cases or want to inspect the parse error.
495    // #[cfg(feature = "recurrence")]
496    // fn get_recurrence(&self) -> Option<rrule::RRuleSet> {
497    //     self.try_recurrence()?.ok()
498    // }
499
500    /// Get recurrence rules, returning a parse error if the `RRULE` property is present
501    /// but invalid.
502    ///
503    /// Returns `None` if no `RRULE` property is present on this component.
504    /// Returns `Some(Err(_))` if an `RRULE` is present but could not be parsed.
505    /// Returns `Some(Ok(_))` if the rule was parsed successfully.
506    ///
507    /// Prefer [`get_recurrence`](EventLike::get_recurrence) for the common case where you
508    /// trust the data source. Use this variant when working with parsed `.ics` input that
509    /// you did not produce yourself and want to surface errors to the caller.
510    #[cfg(feature = "recurrence")]
511    fn get_recurrence(&self) -> Result<rrule::RRuleSet, RecurrenceError> {
512        use std::fmt::Write;
513
514        let mut b = String::new();
515
516        if let Some(dt_start_prop) = self.properties().get("DTSTART") {
517            // rrule's parser only understands DTSTART with an optional TZID parameter.
518            // Other parameters like VALUE=DATE must be omitted, otherwise rrule misinterprets them.
519            if let Some(tzid) = dt_start_prop.params().get("TZID") {
520                writeln!(b, "DTSTART;TZID={}:{}", tzid.value(), dt_start_prop.value()).unwrap();
521            } else {
522                writeln!(b, "DTSTART:{}", dt_start_prop.value()).unwrap();
523            }
524
525            // When there is no RRULE the rrule crate's iterator only yields explicit RDATEs and
526            // never the DTSTART itself.  RFC 5545 ยง3.6.1 says DTSTART is the first instance, so
527            // we add it as an explicit RDATE so callers always see it in the occurrence list.
528            let has_rrule = self.property_value("RRULE").is_some();
529            if !has_rrule {
530                if let Some(tzid) = dt_start_prop.params().get("TZID") {
531                    writeln!(b, "RDATE;TZID={}:{}", tzid.value(), dt_start_prop.value()).unwrap();
532                } else {
533                    writeln!(b, "RDATE:{}", dt_start_prop.value()).unwrap();
534                }
535            }
536        };
537
538        if let Some(rrule_str) = self.property_value("RRULE") {
539            writeln!(b, "RRULE:{rrule_str}").unwrap();
540        }
541
542        if let Some(rdates) = self.multi_properties().get("RDATE") {
543            for rdate in rdates.iter().filter_map(|p| p.to_line().ok()) {
544                writeln!(b, "{rdate}").unwrap();
545            }
546        }
547
548        if let Some(exdates) = self.multi_properties().get("EXDATE") {
549            for exdate in exdates.iter().filter_map(|p| p.to_line().ok()) {
550                writeln!(b, "{exdate}").unwrap();
551            }
552        }
553
554        b.parse::<rrule::RRuleSet>().map_err(RecurrenceError::Rule)
555    }
556
557    /// Add an RDATE to this event
558    ///
559    /// [3.8.5.2.  Recurrence Date-Times](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.5.2)
560    fn rdate<T: Into<DatePerhapsTime>>(&mut self, rdate: T) -> &mut Self {
561        self.append_multi_property(rdate.into().to_property("RDATE"))
562    }
563
564    /// Add an EXDATE
565    ///
566    /// [3.8.5.1.  Exception Date-Times](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.5.1)
567    fn exdate<T: Into<DatePerhapsTime>>(&mut self, exdate: T) -> &mut Self {
568        self.append_multi_property(exdate.into().to_property("EXDATE"))
569    }
570
571    /// Set the ALARM
572    /// [3.6.6.  Alarm Component](https://datatracker.ietf.org/doc/html/rfc5545#section-3.6.6)
573    fn alarm<A: Into<Alarm>>(&mut self, alarm: A) -> &mut Self {
574        let alarm: Alarm = alarm.into();
575        self.append_component(alarm)
576    }
577}
578
579macro_rules! event_impl {
580    ($t:ty) => {
581        impl EventLike for $t {}
582    };
583}
584
585macro_rules! component_impl {
586    ($t:ty, $kind:expr) => {
587        impl Component for $t {
588            /// Tells you what kind of [`Component`] this is
589            ///
590            /// Might be [`VEVENT`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.6.1), [`VTODO`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.6.2), [`VALARM`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.6.6) etc
591            fn component_kind(&self) -> String {
592                $kind
593            }
594
595            /// Read-only access to `properties`
596            fn properties(&self) -> &BTreeMap<String, Property> {
597                &self.inner.properties
598            }
599
600            /// Read-only access to `properties`
601            fn components(&self) -> &[Other] {
602                &self.inner.components
603            }
604
605            /// Read-only access to `multi_properties`
606            fn multi_properties(&self) -> &BTreeMap<String, Vec<Property>> {
607                &self.inner.multi_properties
608            }
609
610            /// Adds a [`Property`]
611            fn append_property(&mut self, property: impl Into<Property>) -> &mut Self {
612                let property = property.into();
613                self.inner
614                    .properties
615                    .insert(property.key().to_owned(), property);
616                self
617            }
618
619            /// Appends a [`Component`]
620            fn append_component(&mut self, child: impl Into<Other>) -> &mut Self {
621                self.inner.components.push(child.into());
622                self
623            }
624
625            /// Adds a [`Property`] of which there may be many
626            fn append_multi_property(&mut self, property: impl Into<Property>) -> &mut Self {
627                self.inner.insert_multi(property);
628                self
629            }
630
631            /// Removes a [`Property`] by its key if it exists
632            fn remove_property(&mut self, key: &str) -> &mut Self {
633                self.inner.properties.remove(key);
634                self
635            }
636
637            /// Removes a multi-property by its key if it exists
638            fn remove_multi_property(&mut self, key: &str) -> &mut Self {
639                self.inner.multi_properties.remove(key);
640                self
641            }
642        }
643
644        impl From<InnerComponent> for $t {
645            fn from(inner: InnerComponent) -> $t {
646                Self { inner }
647            }
648        }
649        impl From<$t> for Other {
650            fn from(val: $t) -> Self {
651                (val.component_kind(), val.inner).into()
652            }
653        }
654
655        impl TryInto<String> for $t {
656            type Error = std::fmt::Error;
657
658            fn try_into(self) -> Result<String, Self::Error> {
659                self.try_into_string()
660            }
661        }
662    };
663}
664
665component_impl! { Event, String::from("VEVENT") }
666event_impl! { Event }
667
668component_impl! { Todo , String::from("VTODO")}
669event_impl! { Todo}
670
671component_impl! { Venue , String::from("VVENUE")}
672component_impl! { Alarm, String::from("VALARM") }
673
674#[cfg(test)]
675mod tests {
676    use chrono::TimeZone;
677
678    use super::*;
679
680    #[test]
681    #[cfg(feature = "parser")]
682    fn get_url() {
683        let url = "http://hoodie.de/";
684        let event = Event::new().url(url).done();
685
686        let serialized = event.to_string();
687        let reparsed =
688            Other::from(crate::parser::Component::<'_>::try_from(serialized.as_str()).unwrap());
689
690        assert_eq!(event.get_url(), Some(url));
691        assert_eq!(reparsed.get_url(), Some(url));
692    }
693
694    #[test]
695    fn get_properties_unset() {
696        let event = Event::new();
697        assert_eq!(event.get_priority(), None);
698        assert_eq!(event.get_summary(), None);
699        assert_eq!(event.get_description(), None);
700        assert_eq!(event.get_location(), None);
701        assert_eq!(event.get_uid(), None);
702        assert_eq!(event.get_class(), None);
703        assert_eq!(event.get_timestamp(), None);
704        assert_eq!(event.get_last_modified(), None);
705        assert_eq!(event.get_created(), None);
706        assert_eq!(event.get_url(), None);
707    }
708
709    #[test]
710    fn get_properties_set() {
711        let event = Event::new()
712            .priority(5)
713            .summary("summary")
714            .description("description")
715            .location("location")
716            .uid("uid")
717            .class(Class::Private)
718            .url("http://some.test/url")
719            .done();
720        assert_eq!(event.get_priority(), Some(5));
721        assert_eq!(event.get_summary(), Some("summary"));
722        assert_eq!(event.get_description(), Some("description"));
723        assert_eq!(event.get_location(), Some("location"));
724        assert_eq!(event.get_uid(), Some("uid"));
725        assert_eq!(event.get_class(), Some(Class::Private));
726        assert_eq!(event.get_url(), Some("http://some.test/url"));
727    }
728
729    #[test]
730    fn get_properties_remove() {
731        let mut event = Event::new()
732            .priority(5)
733            .summary("summary")
734            .description("description")
735            .location("location")
736            .uid("uid")
737            .class(Class::Private)
738            .url("http://some.test/url")
739            .done();
740        assert_eq!(event.get_priority(), Some(5));
741        assert_eq!(event.get_summary(), Some("summary"));
742        assert_eq!(event.get_description(), Some("description"));
743        assert_eq!(event.get_location(), Some("location"));
744        assert_eq!(event.get_uid(), Some("uid"));
745        assert_eq!(event.get_class(), Some(Class::Private));
746        assert_eq!(event.get_url(), Some("http://some.test/url"));
747
748        event
749            .remove_priority()
750            .remove_summary()
751            .remove_description()
752            .remove_location()
753            .remove_class()
754            .remove_url();
755        assert_eq!(event.get_priority(), None);
756        assert_eq!(event.get_summary(), None);
757        assert_eq!(event.get_description(), None);
758        assert_eq!(event.get_location(), None);
759        assert_eq!(event.get_class(), None);
760        assert_eq!(event.get_url(), None);
761    }
762
763    #[test]
764    fn get_date_times_naive() {
765        let naive_date_time = NaiveDate::from_ymd_opt(2001, 3, 13)
766            .unwrap()
767            .and_hms_opt(14, 15, 16)
768            .unwrap();
769        let event = Event::new()
770            .starts(naive_date_time)
771            .ends(naive_date_time)
772            .done();
773        assert_eq!(event.get_start(), Some(naive_date_time.into()));
774        assert_eq!(event.get_end(), Some(naive_date_time.into()));
775    }
776
777    #[test]
778    fn get_date_times_utc() {
779        let utc_date_time = Utc.with_ymd_and_hms(2001, 3, 13, 14, 15, 16).unwrap();
780        let event = Event::new()
781            .timestamp(utc_date_time)
782            .last_modified(utc_date_time)
783            .created(utc_date_time)
784            .starts(utc_date_time)
785            .ends(utc_date_time)
786            .done();
787        assert_eq!(event.get_timestamp(), Some(utc_date_time));
788        assert_eq!(event.get_last_modified(), Some(utc_date_time));
789        assert_eq!(event.get_created(), Some(utc_date_time));
790        assert_eq!(event.get_start(), Some(utc_date_time.into()));
791        assert_eq!(event.get_end(), Some(utc_date_time.into()));
792    }
793
794    #[test]
795    fn get_date_times_tzid() {
796        let date_time = NaiveDate::from_ymd_opt(2001, 3, 13)
797            .unwrap()
798            .and_hms_opt(14, 15, 16)
799            .unwrap();
800        let date_time_tzid = CalendarDateTime::WithTimezone {
801            date_time,
802            tzid: "Pacific/Auckland".to_string(),
803        };
804        let event = Event::new()
805            .starts(date_time_tzid.clone())
806            .ends(date_time_tzid.clone())
807            .done();
808        assert_eq!(event.get_start(), Some(date_time_tzid.clone().into()));
809        assert_eq!(event.get_end(), Some(date_time_tzid.into()));
810    }
811
812    #[test]
813    fn get_dates_naive() {
814        let naive_date = NaiveDate::from_ymd_opt(2001, 3, 13).unwrap();
815        let event = Event::new().starts(naive_date).ends(naive_date).done();
816        assert_eq!(event.get_start(), Some(naive_date.into()));
817        assert_eq!(event.get_end(), Some(naive_date.into()));
818    }
819
820    #[test]
821    fn exdate_accepts_naive_date() {
822        let naive_date = NaiveDate::from_ymd_opt(2001, 3, 13).unwrap();
823        let event = Event::new().exdate(naive_date).done();
824
825        let exdates = event.multi_properties().get("EXDATE").unwrap();
826        assert_eq!(exdates.len(), 1);
827        // Must serialise with VALUE=DATE parameter and a date-only value
828        let line = exdates.first().unwrap().to_line().unwrap();
829        assert!(
830            line.contains("VALUE=DATE"),
831            "EXDATE should carry VALUE=DATE parameter, got: {line}"
832        );
833        assert_eq!(exdates.first().unwrap().value(), "20010313");
834    }
835
836    #[test]
837    fn rdate_accepts_naive_date() {
838        let naive_date = NaiveDate::from_ymd_opt(2001, 3, 13).unwrap();
839        let event = Event::new().rdate(naive_date).done();
840
841        let rdates = event.multi_properties().get("RDATE").unwrap();
842        assert_eq!(rdates.len(), 1);
843        // Must serialise with VALUE=DATE parameter and a date-only value
844        let line = rdates.first().unwrap().to_line().unwrap();
845        assert!(
846            line.contains("VALUE=DATE"),
847            "RDATE should carry VALUE=DATE parameter, got: {line}"
848        );
849        assert_eq!(rdates.first().unwrap().value(), "20010313");
850    }
851
852    #[test]
853    #[cfg(feature = "recurrence")]
854    fn get_recurrence() {
855        use crate::{Frequency, NWeekday, RRule, Weekday};
856
857        let naive_date = NaiveDate::from_ymd_opt(2001, 3, 13).unwrap();
858
859        let rrule = RRule::default()
860            .count(4)
861            .freq(Frequency::Weekly)
862            .by_weekday(vec![
863                NWeekday::Every(Weekday::Tue),
864                NWeekday::Every(Weekday::Wed),
865            ]);
866
867        let event = Event::new()
868            .starts(naive_date)
869            .ends(naive_date)
870            .recurrence(rrule)
871            .unwrap()
872            .done();
873
874        let output = event
875            .get_recurrence()
876            .expect("event should have a recurrence rule");
877
878        let output_rrules = output.get_rrule();
879
880        assert_eq!(output_rrules.len(), 1);
881        assert_eq!(output_rrules.first().unwrap().get_freq(), Frequency::Weekly);
882        assert_eq!(output_rrules.first().unwrap().get_interval(), 1);
883        assert_eq!(
884            output_rrules.first().unwrap().get_by_weekday(),
885            [NWeekday::Every(Weekday::Tue), NWeekday::Every(Weekday::Wed)]
886        );
887    }
888
889    #[test]
890    #[cfg(feature = "recurrence")]
891    fn no_empty_rdate_or_exdate_added() {
892        use crate::{Frequency, RRule};
893
894        // Create an event with an RRULE, but no RDATE or EXDATE
895        let naive_date = NaiveDate::from_ymd_opt(2026, 3, 30).unwrap();
896        let event = Event::new()
897            .starts(naive_date)
898            .recurrence(RRule::default().count(3).freq(Frequency::Daily))
899            .unwrap()
900            .done();
901
902        let serialized = event.to_string();
903
904        // Ensure no empty RDATE or EXDATE lines are present
905        assert!(!serialized.contains("RDATE:"));
906        assert!(!serialized.contains("EXDATE:"));
907    }
908}