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