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