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