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