Skip to main content

ical/
component.rs

1//! # Components
2//!
3//! A decoded component and the iCalendar component-name vocabulary.
4//!
5//! iCalendar is a tree of components: a `VCALENDAR` holds `VEVENT`, `VTODO`,
6//! `VJOURNAL`, `VFREEBUSY` and `VTIMEZONE` components; an event or to-do holds
7//! `VALARM` components; a time zone holds `STANDARD` and `DAYLIGHT`
8//! subcomponents; and (RFC 9073) a component may hold `PARTICIPANT`,
9//! `VLOCATION` and `VRESOURCE` subcomponents. [`IcalComponent`] is that decoded
10//! node: a name, its properties, and its nested components. The whole calendar
11//! is the [`Ical`](crate::ical::Ical) aggregate, whose root is the `VCALENDAR`.
12//!
13//! A known name is held as the closed [`IcalComponentKind`] identity (its wire
14//! spelling reached through `Deref` and `FromStr`); an unknown one keeps its
15//! verbatim bytes. This module is pure model: no dependency on
16//! [`crate::tree`].
17
18use core::{error, fmt, ops, str};
19
20use alloc::{
21    borrow::Cow,
22    string::{String, ToString},
23    vec::Vec,
24};
25
26use crate::{prop::IcalProp, value::owned};
27
28/// Parse iCalendar component kind error.
29#[derive(Debug)]
30pub struct ParseIcalComponentKindError(
31    /// The iCalendar component that cannot be parsed.
32    String,
33);
34
35impl fmt::Display for ParseIcalComponentKindError {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        write!(f, "Cannot parse iCalendar component `{}`", self.0)
38    }
39}
40
41impl error::Error for ParseIcalComponentKindError {}
42
43/// A decoded component: its name, its properties, and its nested components.
44#[derive(Clone, Debug, PartialEq, Eq)]
45pub struct IcalComponent<'a> {
46    /// The component name (a known kind, or an unknown name kept verbatim).
47    pub name: IcalComponentName<'a>,
48    /// The properties of this component, in source order.
49    pub props: Vec<IcalProp<'a>>,
50    /// The components nested directly within this one, in source order.
51    pub components: Vec<IcalComponent<'a>>,
52}
53
54/// A component name: a known iCalendar name, or an unknown one kept verbatim.
55#[derive(Clone, Debug, PartialEq, Eq)]
56pub enum IcalComponentName<'a> {
57    /// A name in the closed iCalendar vocabulary.
58    Kind(IcalComponentKind),
59    /// Any other name, kept as written.
60    Unknown(Cow<'a, str>),
61}
62
63impl ops::Deref for IcalComponentName<'_> {
64    type Target = str;
65
66    /// The name's wire string: the canonical spelling of a known name, or the
67    /// verbatim text of an unknown one.
68    fn deref(&self) -> &Self::Target {
69        match self {
70            Self::Kind(kind) => kind,
71            Self::Unknown(name) => name,
72        }
73    }
74}
75
76impl From<IcalComponentKind> for IcalComponentName<'_> {
77    fn from(kind: IcalComponentKind) -> Self {
78        Self::Kind(kind)
79    }
80}
81
82impl<'a> From<Cow<'a, str>> for IcalComponentName<'a> {
83    fn from(name: Cow<'a, str>) -> Self {
84        match name.parse().ok() {
85            Some(kind) => Self::Kind(kind),
86            None => Self::Unknown(name),
87        }
88    }
89}
90
91impl<'a> From<&'a str> for IcalComponentName<'a> {
92    fn from(name: &'a str) -> Self {
93        Cow::Borrowed(name).into()
94    }
95}
96
97impl IcalComponentName<'_> {
98    /// The same name with every borrow replaced by an allocation. See
99    /// [`IcalValue::into_owned`](crate::value::IcalValue::into_owned).
100    pub fn into_owned(self) -> IcalComponentName<'static> {
101        match self {
102            Self::Kind(kind) => IcalComponentName::Kind(kind),
103            Self::Unknown(name) => IcalComponentName::Unknown(owned(name)),
104        }
105    }
106}
107
108impl IcalComponent<'_> {
109    /// The same component, and everything nested in it, with every borrow
110    /// replaced by an allocation. See
111    /// [`IcalValue::into_owned`](crate::value::IcalValue::into_owned).
112    pub fn into_owned(self) -> IcalComponent<'static> {
113        IcalComponent {
114            name: self.name.into_owned(),
115            props: self.props.into_iter().map(IcalProp::into_owned).collect(),
116            components: self
117                .components
118                .into_iter()
119                .map(IcalComponent::into_owned)
120                .collect(),
121        }
122    }
123}
124
125/// The closed iCalendar component-name vocabulary, one fieldless variant per
126/// known component. An identity for dispatch and nesting rules; the
127/// open-vocabulary counterpart that also carries unknown names is
128/// [`IcalComponentName`].
129#[derive(Clone, Copy, Debug, PartialEq, Eq)]
130pub enum IcalComponentKind {
131    /// `VCALENDAR`: the calendar envelope (RFC 5545 3.4).
132    VCalendar,
133    /// `VEVENT`: an event (RFC 5545 3.6.1).
134    VEvent,
135    /// `VTODO`: a to-do (RFC 5545 3.6.2).
136    VTodo,
137    /// `VJOURNAL`: a journal entry (RFC 5545 3.6.3).
138    VJournal,
139    /// `VFREEBUSY`: free/busy time (RFC 5545 3.6.4).
140    VFreeBusy,
141    /// `VTIMEZONE`: a time-zone definition (RFC 5545 3.6.5).
142    VTimezone,
143    /// `STANDARD`: a standard-time rule (RFC 5545 3.6.5).
144    Standard,
145    /// `DAYLIGHT`: a daylight-saving-time rule (RFC 5545 3.6.5).
146    Daylight,
147    /// `VALARM`: an alarm (RFC 5545 3.6.6).
148    VAlarm,
149    /// `PARTICIPANT`: a participant (RFC 9073 7.1).
150    Participant,
151    /// `VLOCATION`: a location (RFC 9073 7.2).
152    VLocation,
153    /// `VRESOURCE`: a resource (RFC 9073 7.3).
154    VResource,
155    /// `VAVAILABILITY`: an availability window (RFC 7953 3.1).
156    VAvailability,
157    /// `AVAILABLE`: one available period of a `VAVAILABILITY` (RFC 7953 3.1).
158    Available,
159}
160
161impl IcalComponentKind {
162    /// Every known component kind, for iterating the closed vocabulary.
163    pub const ALL: [Self; 14] = [
164        Self::VCalendar,
165        Self::VEvent,
166        Self::VTodo,
167        Self::VJournal,
168        Self::VFreeBusy,
169        Self::VTimezone,
170        Self::Standard,
171        Self::Daylight,
172        Self::VAlarm,
173        Self::Participant,
174        Self::VLocation,
175        Self::VResource,
176        Self::VAvailability,
177        Self::Available,
178    ];
179}
180
181impl str::FromStr for IcalComponentKind {
182    type Err = ParseIcalComponentKindError;
183
184    /// The known component for a wire name (case-insensitive), or an error.
185    fn from_str(kind: &str) -> Result<Self, Self::Err> {
186        let kind = match kind {
187            kind if kind.eq_ignore_ascii_case("VCALENDAR") => Self::VCalendar,
188            kind if kind.eq_ignore_ascii_case("VEVENT") => Self::VEvent,
189            kind if kind.eq_ignore_ascii_case("VTODO") => Self::VTodo,
190            kind if kind.eq_ignore_ascii_case("VJOURNAL") => Self::VJournal,
191            kind if kind.eq_ignore_ascii_case("VFREEBUSY") => Self::VFreeBusy,
192            kind if kind.eq_ignore_ascii_case("VTIMEZONE") => Self::VTimezone,
193            kind if kind.eq_ignore_ascii_case("STANDARD") => Self::Standard,
194            kind if kind.eq_ignore_ascii_case("DAYLIGHT") => Self::Daylight,
195            kind if kind.eq_ignore_ascii_case("VALARM") => Self::VAlarm,
196            kind if kind.eq_ignore_ascii_case("PARTICIPANT") => Self::Participant,
197            kind if kind.eq_ignore_ascii_case("VLOCATION") => Self::VLocation,
198            kind if kind.eq_ignore_ascii_case("VRESOURCE") => Self::VResource,
199            kind if kind.eq_ignore_ascii_case("VAVAILABILITY") => Self::VAvailability,
200            kind if kind.eq_ignore_ascii_case("AVAILABLE") => Self::Available,
201            _ => return Err(ParseIcalComponentKindError(kind.to_string())),
202        };
203
204        Ok(kind)
205    }
206}
207
208impl ops::Deref for IcalComponentKind {
209    type Target = str;
210
211    fn deref(&self) -> &Self::Target {
212        match self {
213            Self::VCalendar => "VCALENDAR",
214            Self::VEvent => "VEVENT",
215            Self::VTodo => "VTODO",
216            Self::VJournal => "VJOURNAL",
217            Self::VFreeBusy => "VFREEBUSY",
218            Self::VTimezone => "VTIMEZONE",
219            Self::Standard => "STANDARD",
220            Self::Daylight => "DAYLIGHT",
221            Self::VAlarm => "VALARM",
222            Self::Participant => "PARTICIPANT",
223            Self::VLocation => "VLOCATION",
224            Self::VResource => "VRESOURCE",
225            Self::VAvailability => "VAVAILABILITY",
226            Self::Available => "AVAILABLE",
227        }
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use core::str::FromStr;
234
235    use crate::component::IcalComponentKind;
236
237    #[test]
238    fn round_trips_every_kind_through_its_wire_name() {
239        for kind in IcalComponentKind::ALL {
240            assert_eq!(IcalComponentKind::from_str(&kind).ok(), Some(kind));
241        }
242        assert_eq!(
243            IcalComponentKind::from_str("vevent").ok(),
244            Some(IcalComponentKind::VEvent),
245        );
246        assert!(IcalComponentKind::from_str("VUNKNOWN").is_err());
247    }
248}