Skip to main content

ical/
value.rs

1//! # Property values
2//!
3//! The decoded value of a property, one variant per iCalendar value kind.
4//!
5//! [`IcalValue`] is the semantic counterpart of a content line's raw value (the
6//! syntactic [`IcalValueNode`](crate::tree::value::node::IcalValueNode)). Most
7//! properties share a small set of kinds: a single text, a text list, a URI, a
8//! date/time, an integer. The genuinely structured ones get a bespoke type in a
9//! submodule here ([`geo::IcalGeo`], [`request_status::IcalRequestStatus`]).
10//! Anything the model does not decode falls back to
11//! [`Unknown`](IcalValue::Unknown), which keeps the raw components so it
12//! round-trips.
13//!
14//! These types carry no wire name and no escaping: the name lives on
15//! [`IcalProp::name`](crate::prop::IcalProp::name), the escaping and framing on
16//! the syntax side, so the decoded model stays free of [`crate::tree`].
17
18pub mod binary;
19pub mod boolean;
20pub mod cal_address;
21pub mod datetime;
22pub mod duration;
23pub mod float;
24pub mod geo;
25pub mod integer;
26pub mod period;
27pub mod recur;
28pub mod request_status;
29pub mod text;
30pub mod uri;
31pub mod utc_offset;
32
33use core::{error, fmt, ops, str};
34
35use alloc::{
36    borrow::Cow,
37    string::{String, ToString},
38    vec::Vec,
39};
40
41use crate::value::{
42    binary::IcalBinary,
43    boolean::IcalBoolean,
44    cal_address::IcalCalAddress,
45    datetime::{IcalDate, IcalDateTime, IcalDateTimeList, IcalTime},
46    duration::IcalDuration,
47    float::IcalFloat,
48    geo::IcalGeo,
49    integer::IcalInteger,
50    period::IcalPeriod,
51    recur::IcalRecur,
52    request_status::IcalRequestStatus,
53    text::{IcalText, IcalTextList},
54    uri::IcalUri,
55    utc_offset::IcalUtcOffset,
56};
57
58/// Parse iCalendar value kind error.
59#[derive(Debug)]
60pub struct ParseIcalValueKindError(
61    /// The iCalendar value type that cannot be parsed.
62    String,
63);
64
65impl fmt::Display for ParseIcalValueKindError {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        write!(f, "Cannot parse iCalendar value type `{}`", self.0)
68    }
69}
70
71impl error::Error for ParseIcalValueKindError {}
72
73/// The closed iCalendar value-type vocabulary (RFC 5545 3.3 and extensions),
74/// one fieldless variant per value kind. It is the discriminant of
75/// [`IcalValue`] (which also has an `Unknown` arm outside this closed set) and
76/// the currency of the prop spec's allowed-values sets.
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub enum IcalValueKind {
79    /// An inline-base64 or URI-reference binary value (RFC 5545 3.3.1).
80    Binary,
81    /// A boolean value (RFC 5545 3.3.2).
82    Boolean,
83    /// A calendar user address, a URI (RFC 5545 3.3.3).
84    CalAddress,
85    /// A calendar date (RFC 5545 3.3.4).
86    Date,
87    /// A date with time (RFC 5545 3.3.5).
88    DateTime,
89    /// A comma-separated list of dates, date-times or periods, as `RDATE` and
90    /// `EXDATE` carry (RFC 5545 3.8.5.1, 3.8.5.2).
91    DateTimeList,
92    /// A duration (RFC 5545 3.3.6).
93    Duration,
94    /// A floating-point number (RFC 5545 3.3.7).
95    Float,
96    /// The structured `GEO` latitude/longitude pair (RFC 5545 3.8.1.6).
97    Geo,
98    /// A signed integer (RFC 5545 3.3.8).
99    Integer,
100    /// A period of time (RFC 5545 3.3.9).
101    Period,
102    /// A recurrence rule (RFC 5545 3.3.10).
103    Recur,
104    /// The structured `REQUEST-STATUS` value (RFC 5545 3.8.8.3).
105    RequestStatus,
106    /// A single text value (RFC 5545 3.3.11).
107    Text,
108    /// A comma-separated text list (RFC 5545 3.3.11).
109    TextList,
110    /// A time of day (RFC 5545 3.3.12).
111    Time,
112    /// A URI (RFC 5545 3.3.13).
113    Uri,
114    /// A UTC offset (RFC 5545 3.3.14).
115    UtcOffset,
116}
117
118impl IcalValueKind {
119    /// Every known value kind, for iterating the closed vocabulary, as
120    /// [`IcalPropKind::ALL`](crate::prop::IcalPropKind::ALL) does for
121    /// properties.
122    pub const ALL: [Self; 18] = [
123        Self::Binary,
124        Self::Boolean,
125        Self::CalAddress,
126        Self::Date,
127        Self::DateTime,
128        Self::DateTimeList,
129        Self::Duration,
130        Self::Float,
131        Self::Geo,
132        Self::Integer,
133        Self::Period,
134        Self::Recur,
135        Self::RequestStatus,
136        Self::Text,
137        Self::TextList,
138        Self::Time,
139        Self::Uri,
140        Self::UtcOffset,
141    ];
142}
143
144impl str::FromStr for IcalValueKind {
145    type Err = ParseIcalValueKindError;
146
147    /// The value kind named by a `VALUE` parameter (case-insensitive). Liberal:
148    /// it maps every wire spelling onto a model kind, leaving membership checks
149    /// to a later validation tier.
150    fn from_str(kind: &str) -> Result<Self, Self::Err> {
151        match kind {
152            kind if kind.eq_ignore_ascii_case("BINARY") => Ok(Self::Binary),
153            kind if kind.eq_ignore_ascii_case("BOOLEAN") => Ok(Self::Boolean),
154            kind if kind.eq_ignore_ascii_case("CAL-ADDRESS") => Ok(Self::CalAddress),
155            kind if kind.eq_ignore_ascii_case("DATE") => Ok(Self::Date),
156            kind if kind.eq_ignore_ascii_case("DATE-TIME") => Ok(Self::DateTime),
157            kind if kind.eq_ignore_ascii_case("DATE-TIME-LIST") => Ok(Self::DateTimeList),
158            kind if kind.eq_ignore_ascii_case("DURATION") => Ok(Self::Duration),
159            kind if kind.eq_ignore_ascii_case("FLOAT") => Ok(Self::Float),
160            kind if kind.eq_ignore_ascii_case("GEO") => Ok(Self::Geo),
161            kind if kind.eq_ignore_ascii_case("INTEGER") => Ok(Self::Integer),
162            kind if kind.eq_ignore_ascii_case("PERIOD") => Ok(Self::Period),
163            kind if kind.eq_ignore_ascii_case("RECUR") => Ok(Self::Recur),
164            kind if kind.eq_ignore_ascii_case("REQUEST-STATUS") => Ok(Self::RequestStatus),
165            kind if kind.eq_ignore_ascii_case("TEXT") => Ok(Self::Text),
166            kind if kind.eq_ignore_ascii_case("TEXT-LIST") => Ok(Self::TextList),
167            kind if kind.eq_ignore_ascii_case("TIME") => Ok(Self::Time),
168            kind if kind.eq_ignore_ascii_case("URI") => Ok(Self::Uri),
169            kind if kind.eq_ignore_ascii_case("UTC-OFFSET") => Ok(Self::UtcOffset),
170            _ => Err(ParseIcalValueKindError(kind.to_string())),
171        }
172    }
173}
174
175impl ops::Deref for IcalValueKind {
176    type Target = str;
177
178    fn deref(&self) -> &Self::Target {
179        match self {
180            Self::Binary => "BINARY",
181            Self::Boolean => "BOOLEAN",
182            Self::CalAddress => "CAL-ADDRESS",
183            Self::Date => "DATE",
184            Self::DateTime => "DATE-TIME",
185            Self::DateTimeList => "DATE-TIME-LIST",
186            Self::Duration => "DURATION",
187            Self::Float => "FLOAT",
188            Self::Geo => "GEO",
189            Self::Integer => "INTEGER",
190            Self::Period => "PERIOD",
191            Self::Recur => "RECUR",
192            Self::RequestStatus => "REQUEST-STATUS",
193            Self::Text => "TEXT",
194            Self::TextList => "TEXT-LIST",
195            Self::Time => "TIME",
196            Self::Uri => "URI",
197            Self::UtcOffset => "UTC-OFFSET",
198        }
199    }
200}
201
202/// A decoded property value: one known kind, or `Unknown` (raw) for anything
203/// the model does not decode.
204#[derive(Clone, Debug, PartialEq, Eq)]
205pub enum IcalValue<'a> {
206    /// A binary value (`ATTACH`, `IMAGE`): a URI reference or inline base64.
207    Binary(IcalBinary<'a>),
208    /// A boolean value.
209    Boolean(IcalBoolean<'a>),
210    /// A calendar user address (`ORGANIZER`, `ATTENDEE`).
211    CalAddress(IcalCalAddress<'a>),
212    /// A calendar date.
213    Date(IcalDate<'a>),
214    /// A date with time (`DTSTAMP`, `DTSTART`, ...).
215    DateTime(IcalDateTime<'a>),
216    /// A list of dates, date-times or periods (`RDATE`, `EXDATE`).
217    DateTimeList(IcalDateTimeList<'a>),
218    /// A duration (`DURATION`, `TRIGGER`).
219    Duration(IcalDuration<'a>),
220    /// A floating-point number.
221    Float(IcalFloat<'a>),
222    /// The structured `GEO` latitude/longitude pair.
223    Geo(IcalGeo<'a>),
224    /// A signed integer (`PRIORITY`, `SEQUENCE`, `PERCENT-COMPLETE`).
225    Integer(IcalInteger<'a>),
226    /// A period of time (`FREEBUSY`).
227    Period(IcalPeriod<'a>),
228    /// A recurrence rule (`RRULE`).
229    Recur(IcalRecur<'a>),
230    /// The structured `REQUEST-STATUS` value.
231    RequestStatus(IcalRequestStatus<'a>),
232    /// A single text value (`SUMMARY`, `DESCRIPTION`, ...).
233    Text(IcalText<'a>),
234    /// A comma-separated text list (`CATEGORIES`, `RESOURCES`).
235    TextList(IcalTextList<'a>),
236    /// A time of day.
237    Time(IcalTime<'a>),
238    /// A URI (`URL`, `TZURL`, `SOURCE`, ...).
239    Uri(IcalUri<'a>),
240    /// A UTC offset (`TZOFFSETFROM`, `TZOFFSETTO`).
241    UtcOffset(IcalUtcOffset<'a>),
242
243    /// Any value the model does not decode, kept as its raw components so it
244    /// round-trips.
245    Unknown(IcalUnknownValue<'a>),
246}
247
248impl IcalValue<'_> {
249    /// The closed [`IcalValueKind`] of this value, or `None` for
250    /// [`Unknown`](IcalValue::Unknown) (which is outside the vocabulary).
251    pub fn kind(&self) -> Option<IcalValueKind> {
252        match self {
253            Self::Binary(_) => Some(IcalValueKind::Binary),
254            Self::Boolean(_) => Some(IcalValueKind::Boolean),
255            Self::CalAddress(_) => Some(IcalValueKind::CalAddress),
256            Self::Date(_) => Some(IcalValueKind::Date),
257            Self::DateTime(_) => Some(IcalValueKind::DateTime),
258            Self::DateTimeList(_) => Some(IcalValueKind::DateTimeList),
259            Self::Duration(_) => Some(IcalValueKind::Duration),
260            Self::Float(_) => Some(IcalValueKind::Float),
261            Self::Geo(_) => Some(IcalValueKind::Geo),
262            Self::Integer(_) => Some(IcalValueKind::Integer),
263            Self::Period(_) => Some(IcalValueKind::Period),
264            Self::Recur(_) => Some(IcalValueKind::Recur),
265            Self::RequestStatus(_) => Some(IcalValueKind::RequestStatus),
266            Self::Text(_) => Some(IcalValueKind::Text),
267            Self::TextList(_) => Some(IcalValueKind::TextList),
268            Self::Time(_) => Some(IcalValueKind::Time),
269            Self::Uri(_) => Some(IcalValueKind::Uri),
270            Self::UtcOffset(_) => Some(IcalValueKind::UtcOffset),
271            Self::Unknown(_) => None,
272        }
273    }
274
275    /// The same value with every borrow replaced by an allocation, so it
276    /// outlives the bytes it was decoded from.
277    ///
278    /// The counterpart of [`Cow::into_owned`](alloc::borrow::Cow::into_owned),
279    /// for a whole value: a calendar read from a buffer that is about to go
280    /// away, or rebuilt from data that was never one line to begin with, needs
281    /// exactly this.
282    pub fn into_owned(self) -> IcalValue<'static> {
283        match self {
284            Self::Binary(IcalBinary::Uri(value)) => {
285                IcalValue::Binary(IcalBinary::Uri(owned(value)))
286            }
287            Self::Binary(IcalBinary::Base64(value)) => {
288                IcalValue::Binary(IcalBinary::Base64(owned(value)))
289            }
290            Self::Boolean(value) => IcalValue::Boolean(IcalBoolean(owned(value.0))),
291            Self::CalAddress(value) => IcalValue::CalAddress(IcalCalAddress(owned(value.0))),
292            Self::Date(value) => IcalValue::Date(IcalDate(owned(value.0))),
293            Self::DateTime(value) => IcalValue::DateTime(IcalDateTime(owned(value.0))),
294            Self::DateTimeList(value) => {
295                IcalValue::DateTimeList(IcalDateTimeList(value.0.into_iter().map(owned).collect()))
296            }
297            Self::Duration(value) => IcalValue::Duration(IcalDuration(owned(value.0))),
298            Self::Float(value) => IcalValue::Float(IcalFloat(owned(value.0))),
299            Self::Geo(value) => IcalValue::Geo(IcalGeo {
300                latitude: owned(value.latitude),
301                longitude: owned(value.longitude),
302            }),
303            Self::Integer(value) => IcalValue::Integer(IcalInteger(owned(value.0))),
304            Self::Period(value) => IcalValue::Period(IcalPeriod(owned(value.0))),
305            Self::Recur(value) => IcalValue::Recur(IcalRecur(owned(value.0))),
306            Self::RequestStatus(value) => IcalValue::RequestStatus(IcalRequestStatus {
307                code: owned(value.code),
308                description: owned(value.description),
309                extra: owned(value.extra),
310            }),
311            Self::Text(value) => IcalValue::Text(IcalText(owned(value.0))),
312            Self::TextList(value) => {
313                IcalValue::TextList(IcalTextList(value.0.into_iter().map(owned).collect()))
314            }
315            Self::Time(value) => IcalValue::Time(IcalTime(owned(value.0))),
316            Self::Uri(value) => IcalValue::Uri(IcalUri(owned(value.0))),
317            Self::UtcOffset(value) => IcalValue::UtcOffset(IcalUtcOffset(owned(value.0))),
318            Self::Unknown(value) => IcalValue::Unknown(IcalUnknownValue {
319                components: value
320                    .components
321                    .into_iter()
322                    .map(|component| component.into_iter().map(owned).collect())
323                    .collect(),
324            }),
325        }
326    }
327}
328
329/// One borrowed string as an owned one, the step every `into_owned` in the
330/// model is made of.
331pub(crate) fn owned(text: Cow<'_, str>) -> Cow<'static, str> {
332    Cow::Owned(text.into_owned())
333}
334
335/// An undecoded property value: its unescaped components, in source order. The
336/// property name lives on [`IcalProp::name`](crate::prop::IcalProp::name).
337#[derive(Clone, Debug, Default, PartialEq, Eq)]
338pub struct IcalUnknownValue<'a> {
339    /// The value, as components of values.
340    pub components: Vec<Vec<Cow<'a, str>>>,
341}
342
343#[cfg(test)]
344mod tests {
345    use core::str::FromStr;
346
347    use crate::value::{IcalUnknownValue, IcalValue, IcalValueKind, text::IcalText};
348
349    #[test]
350    fn reports_the_kind_of_a_value_and_none_for_unknown() {
351        assert_eq!(
352            IcalValue::Text(IcalText::default()).kind(),
353            Some(IcalValueKind::Text),
354        );
355        assert_eq!(IcalValue::Unknown(IcalUnknownValue::default()).kind(), None);
356    }
357
358    #[test]
359    fn maps_value_param_strings_liberally_and_case_insensitively() {
360        assert_eq!("URI".parse().ok(), Some(IcalValueKind::Uri));
361        assert_eq!("date-time".parse().ok(), Some(IcalValueKind::DateTime));
362        assert!(IcalValueKind::from_str("bogus").is_err());
363    }
364}