Skip to main content

ical/
prop.rs

1//! # Properties
2//!
3//! A decoded property and the iCalendar property-name vocabulary.
4//!
5//! An [`IcalProp`] is an [`IcalPropName`], a list of parameters, and a decoded
6//! value. The name is stored explicitly because many properties share one
7//! [`IcalValue`] kind: `SUMMARY` and `LOCATION` both decode to text, so the
8//! value alone cannot say which property it is. A known name is held as the
9//! closed [`IcalPropKind`] identity (its wire spelling reached through `Deref`
10//! and `FromStr`); an unknown one keeps its verbatim bytes.
11//!
12//! Build a property directly from its public fields; strict, spec-checked
13//! construction lives in the syntax layer
14//! ([`IcalPropBuilder`](crate::tree::ical::builder::IcalPropBuilder)), which
15//! this module does not depend on: pure model, no [`crate::tree`].
16
17use core::{error, fmt, ops, str};
18
19use alloc::{
20    borrow::Cow,
21    string::{String, ToString},
22    vec::Vec,
23};
24
25use crate::{
26    param::IcalParam,
27    value::{IcalValue, owned},
28};
29
30/// Parse iCalendar property kind error.
31#[derive(Debug)]
32pub struct ParseIcalPropKindError(
33    /// The iCalendar property that cannot be parsed.
34    String,
35);
36
37impl fmt::Display for ParseIcalPropKindError {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        write!(f, "Cannot parse iCalendar property `{}`", self.0)
40    }
41}
42
43impl error::Error for ParseIcalPropKindError {}
44
45/// A decoded property: its wire name, its parameters, and its decoded value.
46#[derive(Clone, Debug, PartialEq, Eq)]
47pub struct IcalProp<'a> {
48    /// The property name (a known kind, or an unknown name kept verbatim).
49    pub name: IcalPropName<'a>,
50    /// The parameters decorating the property.
51    pub params: Vec<IcalParam<'a>>,
52    /// The decoded value.
53    pub value: IcalValue<'a>,
54}
55
56/// A property name: a known iCalendar name, or an unknown one kept verbatim.
57///
58/// Known names normalise to their canonical [`IcalPropKind`] spelling; unknown
59/// names keep their exact bytes so they round-trip.
60#[derive(Clone, Debug, PartialEq, Eq)]
61pub enum IcalPropName<'a> {
62    /// A name in the closed iCalendar vocabulary.
63    Kind(IcalPropKind),
64    /// Any other name, kept as written.
65    Unknown(Cow<'a, str>),
66}
67
68impl ops::Deref for IcalPropName<'_> {
69    type Target = str;
70
71    /// The name's wire string: the canonical spelling of a known name, or the
72    /// verbatim text of an unknown one.
73    fn deref(&self) -> &Self::Target {
74        match self {
75            Self::Kind(kind) => kind,
76            Self::Unknown(name) => name,
77        }
78    }
79}
80
81impl From<IcalPropKind> for IcalPropName<'_> {
82    fn from(kind: IcalPropKind) -> Self {
83        Self::Kind(kind)
84    }
85}
86
87impl From<&IcalPropKind> for IcalPropName<'_> {
88    fn from(kind: &IcalPropKind) -> Self {
89        Self::Kind(*kind)
90    }
91}
92
93impl<'a> From<Cow<'a, str>> for IcalPropName<'a> {
94    fn from(name: Cow<'a, str>) -> Self {
95        match name.parse().ok() {
96            Some(kind) => Self::Kind(kind),
97            None => Self::Unknown(name),
98        }
99    }
100}
101
102impl<'a> From<&'a str> for IcalPropName<'a> {
103    fn from(name: &'a str) -> Self {
104        Cow::Borrowed(name).into()
105    }
106}
107
108impl IcalPropName<'_> {
109    /// The same name with every borrow replaced by an allocation. See
110    /// [`IcalValue::into_owned`](crate::value::IcalValue::into_owned).
111    pub fn into_owned(self) -> IcalPropName<'static> {
112        match self {
113            Self::Kind(kind) => IcalPropName::Kind(kind),
114            Self::Unknown(name) => IcalPropName::Unknown(owned(name)),
115        }
116    }
117}
118
119impl IcalProp<'_> {
120    /// The same property with every borrow replaced by an allocation, so it
121    /// outlives the bytes it was decoded from. See
122    /// [`IcalValue::into_owned`](crate::value::IcalValue::into_owned).
123    pub fn into_owned(self) -> IcalProp<'static> {
124        IcalProp {
125            name: self.name.into_owned(),
126            params: self.params.into_iter().map(IcalParam::into_owned).collect(),
127            value: self.value.into_owned(),
128        }
129    }
130}
131
132/// The closed iCalendar property-name vocabulary, one fieldless variant per
133/// known property. An identity for dispatch and allowed-sets; the
134/// open-vocabulary counterpart that also carries unknown names is
135/// [`IcalPropName`]. Covers RFC 5545, 7986, 9073 and 9074, plus the vCalendar
136/// 1.0 legacy alarm properties.
137#[derive(Clone, Copy, Debug, PartialEq, Eq)]
138pub enum IcalPropKind {
139    /// `CALSCALE`: calendar scale (RFC 5545 3.7.1).
140    CalScale,
141    /// `METHOD`: iTIP method (RFC 5545 3.7.2).
142    Method,
143    /// `PRODID`: product identifier (RFC 5545 3.7.3).
144    ProdId,
145    /// `ATTACH`: an associated document (RFC 5545 3.8.1.1).
146    Attach,
147    /// `CATEGORIES`: categories or tags (RFC 5545 3.8.1.2).
148    Categories,
149    /// `CLASS`: access classification (RFC 5545 3.8.1.3).
150    Class,
151    /// `COMMENT`: a comment (RFC 5545 3.8.1.4).
152    Comment,
153    /// `DESCRIPTION`: a full description (RFC 5545 3.8.1.5).
154    Description,
155    /// `GEO`: geographic position (RFC 5545 3.8.1.6).
156    Geo,
157    /// `LOCATION`: the intended venue (RFC 5545 3.8.1.7).
158    Location,
159    /// `PERCENT-COMPLETE`: to-do completion percentage (RFC 5545 3.8.1.8).
160    PercentComplete,
161    /// `PRIORITY`: relative priority (RFC 5545 3.8.1.9).
162    Priority,
163    /// `RESOURCES`: equipment or resources (RFC 5545 3.8.1.10).
164    Resources,
165    /// `STATUS`: overall status (RFC 5545 3.8.1.11).
166    Status,
167    /// `SUMMARY`: a short summary (RFC 5545 3.8.1.12).
168    Summary,
169    /// `COMPLETED`: date/time a to-do was completed (RFC 5545 3.8.2.1).
170    Completed,
171    /// `DTEND`: end date/time (RFC 5545 3.8.2.2).
172    DtEnd,
173    /// `DUE`: to-do due date/time (RFC 5545 3.8.2.3).
174    Due,
175    /// `DTSTART`: start date/time (RFC 5545 3.8.2.4).
176    DtStart,
177    /// `DURATION`: a duration (RFC 5545 3.8.2.5).
178    Duration,
179    /// `FREEBUSY`: free/busy time (RFC 5545 3.8.2.6).
180    FreeBusy,
181    /// `TRANSP`: time transparency (RFC 5545 3.8.2.7).
182    Transp,
183    /// `TZID`: time-zone identifier (RFC 5545 3.8.3.1).
184    TzId,
185    /// `TZNAME`: time-zone name (RFC 5545 3.8.3.2).
186    TzName,
187    /// `TZOFFSETFROM`: offset in use before a transition (RFC 5545 3.8.3.3).
188    TzOffsetFrom,
189    /// `TZOFFSETTO`: offset in use after a transition (RFC 5545 3.8.3.4).
190    TzOffsetTo,
191    /// `TZURL`: time-zone definition URL (RFC 5545 3.8.3.5).
192    TzUrl,
193    /// `ATTENDEE`: an attendee (RFC 5545 3.8.4.1).
194    Attendee,
195    /// `CONTACT`: contact information (RFC 5545 3.8.4.2).
196    Contact,
197    /// `ORGANIZER`: the organizer (RFC 5545 3.8.4.3).
198    Organizer,
199    /// `RECURRENCE-ID`: identifies a recurrence instance (RFC 5545 3.8.4.4).
200    RecurrenceId,
201    /// `RELATED-TO`: a relationship to another component (RFC 5545 3.8.4.5).
202    RelatedTo,
203    /// `URL`: an associated URL (RFC 5545 3.8.4.6).
204    Url,
205    /// `UID`: unique identifier (RFC 5545 3.8.4.7).
206    Uid,
207    /// `EXDATE`: excepted recurrence dates (RFC 5545 3.8.5.1).
208    ExDate,
209    /// `RDATE`: recurrence dates (RFC 5545 3.8.5.2).
210    RDate,
211    /// `RRULE`: recurrence rule (RFC 5545 3.8.5.3).
212    RRule,
213    /// `EXRULE`: exception rule (RFC 2445 4.8.5.2; deprecated in RFC 5545).
214    ExRule,
215    /// `ACTION`: alarm action (RFC 5545 3.8.6.1).
216    Action,
217    /// `REPEAT`: alarm repeat count (RFC 5545 3.8.6.2).
218    Repeat,
219    /// `TRIGGER`: alarm trigger (RFC 5545 3.8.6.3).
220    Trigger,
221    /// `CREATED`: creation date/time (RFC 5545 3.8.7.1).
222    Created,
223    /// `DTSTAMP`: object creation/last-revision timestamp (RFC 5545 3.8.7.2).
224    DtStamp,
225    /// `LAST-MODIFIED`: last-modification date/time (RFC 5545 3.8.7.3).
226    LastModified,
227    /// `SEQUENCE`: revision sequence number (RFC 5545 3.8.7.4).
228    Sequence,
229    /// `REQUEST-STATUS`: scheduling request status (RFC 5545 3.8.8.3).
230    RequestStatus,
231    /// `NAME`: calendar display name (RFC 7986 5.1).
232    Name,
233    /// `REFRESH-INTERVAL`: suggested refresh interval (RFC 7986 5.7).
234    RefreshInterval,
235    /// `SOURCE`: calendar source URL (RFC 7986 5.8).
236    Source,
237    /// `COLOR`: a display colour (RFC 7986 5.9).
238    Color,
239    /// `IMAGE`: an associated image (RFC 7986 5.10).
240    Image,
241    /// `CONFERENCE`: conference access information (RFC 7986 5.11).
242    Conference,
243    /// `PARTICIPANT-TYPE`: participant type (RFC 9073 6.2).
244    ParticipantType,
245    /// `RESOURCE-TYPE`: resource type (RFC 9073 6.3).
246    ResourceType,
247    /// `CALENDAR-ADDRESS`: participant calendar address (RFC 9073 6.4).
248    CalendarAddress,
249    /// `LOCATION-TYPE`: location type (RFC 9073 6.1).
250    LocationType,
251    /// `STRUCTURED-DATA`: structured ancillary data (RFC 9073 6.6).
252    StructuredData,
253    /// `LINK`: a typed link to a related resource (RFC 9253 8.1).
254    Link,
255    /// `REFID`: a reference identifier grouping components (RFC 9253 8.2).
256    Refid,
257    /// `CONCEPT`: a categorisation of a component (RFC 9253 8.3).
258    Concept,
259    /// `BUSYTYPE`: the busy state an availability window states (RFC 7953 3.2).
260    BusyType,
261    /// `STYLED-DESCRIPTION`: rich-text description (RFC 9073 6.5).
262    StyledDescription,
263    /// `ACKNOWLEDGED`: alarm acknowledgement time (RFC 9074 6).
264    Acknowledged,
265    /// `PROXIMITY`: location-proximity trigger (RFC 9074 8).
266    Proximity,
267    /// `TZ`: time-zone offset (vCalendar 1.0).
268    Tz,
269    /// `AALARM`: audio alarm (vCalendar 1.0).
270    AAlarm,
271    /// `DALARM`: display alarm (vCalendar 1.0).
272    DAlarm,
273    /// `MALARM`: mail alarm (vCalendar 1.0).
274    MAlarm,
275    /// `PALARM`: procedure alarm (vCalendar 1.0).
276    PAlarm,
277    /// `RNUM`: recurrence-count number (vCalendar 1.0).
278    RNum,
279}
280
281impl IcalPropKind {
282    /// Every known property kind, for iterating the closed vocabulary (e.g. a
283    /// validator checking which required properties are absent).
284    pub const ALL: [Self; 70] = [
285        Self::CalScale,
286        Self::Method,
287        Self::ProdId,
288        Self::Attach,
289        Self::Categories,
290        Self::Class,
291        Self::Comment,
292        Self::Description,
293        Self::Geo,
294        Self::Location,
295        Self::PercentComplete,
296        Self::Priority,
297        Self::Resources,
298        Self::Status,
299        Self::Summary,
300        Self::Completed,
301        Self::DtEnd,
302        Self::Due,
303        Self::DtStart,
304        Self::Duration,
305        Self::FreeBusy,
306        Self::Transp,
307        Self::TzId,
308        Self::TzName,
309        Self::TzOffsetFrom,
310        Self::TzOffsetTo,
311        Self::TzUrl,
312        Self::Attendee,
313        Self::Contact,
314        Self::Organizer,
315        Self::RecurrenceId,
316        Self::RelatedTo,
317        Self::Url,
318        Self::Uid,
319        Self::ExDate,
320        Self::RDate,
321        Self::RRule,
322        Self::ExRule,
323        Self::Action,
324        Self::Repeat,
325        Self::Trigger,
326        Self::Created,
327        Self::DtStamp,
328        Self::LastModified,
329        Self::Sequence,
330        Self::RequestStatus,
331        Self::Name,
332        Self::RefreshInterval,
333        Self::Source,
334        Self::Color,
335        Self::Image,
336        Self::Conference,
337        Self::ParticipantType,
338        Self::ResourceType,
339        Self::CalendarAddress,
340        Self::LocationType,
341        Self::StructuredData,
342        Self::Link,
343        Self::Refid,
344        Self::Concept,
345        Self::BusyType,
346        Self::StyledDescription,
347        Self::Acknowledged,
348        Self::Proximity,
349        Self::Tz,
350        Self::AAlarm,
351        Self::DAlarm,
352        Self::MAlarm,
353        Self::PAlarm,
354        Self::RNum,
355    ];
356}
357
358impl str::FromStr for IcalPropKind {
359    type Err = ParseIcalPropKindError;
360
361    /// The known property for a wire name (case-insensitive), or an error.
362    fn from_str(kind: &str) -> Result<Self, Self::Err> {
363        let kind = match kind {
364            kind if kind.eq_ignore_ascii_case("CALSCALE") => Self::CalScale,
365            kind if kind.eq_ignore_ascii_case("METHOD") => Self::Method,
366            kind if kind.eq_ignore_ascii_case("PRODID") => Self::ProdId,
367            kind if kind.eq_ignore_ascii_case("ATTACH") => Self::Attach,
368            kind if kind.eq_ignore_ascii_case("CATEGORIES") => Self::Categories,
369            kind if kind.eq_ignore_ascii_case("CLASS") => Self::Class,
370            kind if kind.eq_ignore_ascii_case("COMMENT") => Self::Comment,
371            kind if kind.eq_ignore_ascii_case("DESCRIPTION") => Self::Description,
372            kind if kind.eq_ignore_ascii_case("GEO") => Self::Geo,
373            kind if kind.eq_ignore_ascii_case("LOCATION") => Self::Location,
374            kind if kind.eq_ignore_ascii_case("PERCENT-COMPLETE") => Self::PercentComplete,
375            kind if kind.eq_ignore_ascii_case("PRIORITY") => Self::Priority,
376            kind if kind.eq_ignore_ascii_case("RESOURCES") => Self::Resources,
377            kind if kind.eq_ignore_ascii_case("STATUS") => Self::Status,
378            kind if kind.eq_ignore_ascii_case("SUMMARY") => Self::Summary,
379            kind if kind.eq_ignore_ascii_case("COMPLETED") => Self::Completed,
380            kind if kind.eq_ignore_ascii_case("DTEND") => Self::DtEnd,
381            kind if kind.eq_ignore_ascii_case("DUE") => Self::Due,
382            kind if kind.eq_ignore_ascii_case("DTSTART") => Self::DtStart,
383            kind if kind.eq_ignore_ascii_case("DURATION") => Self::Duration,
384            kind if kind.eq_ignore_ascii_case("FREEBUSY") => Self::FreeBusy,
385            kind if kind.eq_ignore_ascii_case("TRANSP") => Self::Transp,
386            kind if kind.eq_ignore_ascii_case("TZID") => Self::TzId,
387            kind if kind.eq_ignore_ascii_case("TZNAME") => Self::TzName,
388            kind if kind.eq_ignore_ascii_case("TZOFFSETFROM") => Self::TzOffsetFrom,
389            kind if kind.eq_ignore_ascii_case("TZOFFSETTO") => Self::TzOffsetTo,
390            kind if kind.eq_ignore_ascii_case("TZURL") => Self::TzUrl,
391            kind if kind.eq_ignore_ascii_case("ATTENDEE") => Self::Attendee,
392            kind if kind.eq_ignore_ascii_case("CONTACT") => Self::Contact,
393            kind if kind.eq_ignore_ascii_case("ORGANIZER") => Self::Organizer,
394            kind if kind.eq_ignore_ascii_case("RECURRENCE-ID") => Self::RecurrenceId,
395            kind if kind.eq_ignore_ascii_case("RELATED-TO") => Self::RelatedTo,
396            kind if kind.eq_ignore_ascii_case("URL") => Self::Url,
397            kind if kind.eq_ignore_ascii_case("UID") => Self::Uid,
398            kind if kind.eq_ignore_ascii_case("EXDATE") => Self::ExDate,
399            kind if kind.eq_ignore_ascii_case("RDATE") => Self::RDate,
400            kind if kind.eq_ignore_ascii_case("RRULE") => Self::RRule,
401            kind if kind.eq_ignore_ascii_case("EXRULE") => Self::ExRule,
402            kind if kind.eq_ignore_ascii_case("ACTION") => Self::Action,
403            kind if kind.eq_ignore_ascii_case("REPEAT") => Self::Repeat,
404            kind if kind.eq_ignore_ascii_case("TRIGGER") => Self::Trigger,
405            kind if kind.eq_ignore_ascii_case("CREATED") => Self::Created,
406            kind if kind.eq_ignore_ascii_case("DTSTAMP") => Self::DtStamp,
407            kind if kind.eq_ignore_ascii_case("LAST-MODIFIED") => Self::LastModified,
408            kind if kind.eq_ignore_ascii_case("SEQUENCE") => Self::Sequence,
409            kind if kind.eq_ignore_ascii_case("REQUEST-STATUS") => Self::RequestStatus,
410            kind if kind.eq_ignore_ascii_case("NAME") => Self::Name,
411            kind if kind.eq_ignore_ascii_case("REFRESH-INTERVAL") => Self::RefreshInterval,
412            kind if kind.eq_ignore_ascii_case("SOURCE") => Self::Source,
413            kind if kind.eq_ignore_ascii_case("COLOR") => Self::Color,
414            kind if kind.eq_ignore_ascii_case("IMAGE") => Self::Image,
415            kind if kind.eq_ignore_ascii_case("CONFERENCE") => Self::Conference,
416            kind if kind.eq_ignore_ascii_case("PARTICIPANT-TYPE") => Self::ParticipantType,
417            kind if kind.eq_ignore_ascii_case("RESOURCE-TYPE") => Self::ResourceType,
418            kind if kind.eq_ignore_ascii_case("CALENDAR-ADDRESS") => Self::CalendarAddress,
419            kind if kind.eq_ignore_ascii_case("LOCATION-TYPE") => Self::LocationType,
420            kind if kind.eq_ignore_ascii_case("STRUCTURED-DATA") => Self::StructuredData,
421            kind if kind.eq_ignore_ascii_case("LINK") => Self::Link,
422            kind if kind.eq_ignore_ascii_case("REFID") => Self::Refid,
423            kind if kind.eq_ignore_ascii_case("CONCEPT") => Self::Concept,
424            kind if kind.eq_ignore_ascii_case("BUSYTYPE") => Self::BusyType,
425            kind if kind.eq_ignore_ascii_case("STYLED-DESCRIPTION") => Self::StyledDescription,
426            kind if kind.eq_ignore_ascii_case("ACKNOWLEDGED") => Self::Acknowledged,
427            kind if kind.eq_ignore_ascii_case("PROXIMITY") => Self::Proximity,
428            kind if kind.eq_ignore_ascii_case("TZ") => Self::Tz,
429            kind if kind.eq_ignore_ascii_case("AALARM") => Self::AAlarm,
430            kind if kind.eq_ignore_ascii_case("DALARM") => Self::DAlarm,
431            kind if kind.eq_ignore_ascii_case("MALARM") => Self::MAlarm,
432            kind if kind.eq_ignore_ascii_case("PALARM") => Self::PAlarm,
433            kind if kind.eq_ignore_ascii_case("RNUM") => Self::RNum,
434            _ => return Err(ParseIcalPropKindError(kind.to_string())),
435        };
436
437        Ok(kind)
438    }
439}
440
441impl ops::Deref for IcalPropKind {
442    type Target = str;
443
444    fn deref(&self) -> &Self::Target {
445        match self {
446            Self::CalScale => "CALSCALE",
447            Self::Method => "METHOD",
448            Self::ProdId => "PRODID",
449            Self::Attach => "ATTACH",
450            Self::Categories => "CATEGORIES",
451            Self::Class => "CLASS",
452            Self::Comment => "COMMENT",
453            Self::Description => "DESCRIPTION",
454            Self::Geo => "GEO",
455            Self::Location => "LOCATION",
456            Self::PercentComplete => "PERCENT-COMPLETE",
457            Self::Priority => "PRIORITY",
458            Self::Resources => "RESOURCES",
459            Self::Status => "STATUS",
460            Self::Summary => "SUMMARY",
461            Self::Completed => "COMPLETED",
462            Self::DtEnd => "DTEND",
463            Self::Due => "DUE",
464            Self::DtStart => "DTSTART",
465            Self::Duration => "DURATION",
466            Self::FreeBusy => "FREEBUSY",
467            Self::Transp => "TRANSP",
468            Self::TzId => "TZID",
469            Self::TzName => "TZNAME",
470            Self::TzOffsetFrom => "TZOFFSETFROM",
471            Self::TzOffsetTo => "TZOFFSETTO",
472            Self::TzUrl => "TZURL",
473            Self::Attendee => "ATTENDEE",
474            Self::Contact => "CONTACT",
475            Self::Organizer => "ORGANIZER",
476            Self::RecurrenceId => "RECURRENCE-ID",
477            Self::RelatedTo => "RELATED-TO",
478            Self::Url => "URL",
479            Self::Uid => "UID",
480            Self::ExDate => "EXDATE",
481            Self::RDate => "RDATE",
482            Self::RRule => "RRULE",
483            Self::ExRule => "EXRULE",
484            Self::Action => "ACTION",
485            Self::Repeat => "REPEAT",
486            Self::Trigger => "TRIGGER",
487            Self::Created => "CREATED",
488            Self::DtStamp => "DTSTAMP",
489            Self::LastModified => "LAST-MODIFIED",
490            Self::Sequence => "SEQUENCE",
491            Self::RequestStatus => "REQUEST-STATUS",
492            Self::Name => "NAME",
493            Self::RefreshInterval => "REFRESH-INTERVAL",
494            Self::Source => "SOURCE",
495            Self::Color => "COLOR",
496            Self::Image => "IMAGE",
497            Self::Conference => "CONFERENCE",
498            Self::ParticipantType => "PARTICIPANT-TYPE",
499            Self::ResourceType => "RESOURCE-TYPE",
500            Self::CalendarAddress => "CALENDAR-ADDRESS",
501            Self::LocationType => "LOCATION-TYPE",
502            Self::StructuredData => "STRUCTURED-DATA",
503            Self::Link => "LINK",
504            Self::Refid => "REFID",
505            Self::Concept => "CONCEPT",
506            Self::BusyType => "BUSYTYPE",
507            Self::StyledDescription => "STYLED-DESCRIPTION",
508            Self::Acknowledged => "ACKNOWLEDGED",
509            Self::Proximity => "PROXIMITY",
510            Self::Tz => "TZ",
511            Self::AAlarm => "AALARM",
512            Self::DAlarm => "DALARM",
513            Self::MAlarm => "MALARM",
514            Self::PAlarm => "PALARM",
515            Self::RNum => "RNUM",
516        }
517    }
518}
519
520#[cfg(test)]
521mod tests {
522    use core::str::FromStr;
523
524    use alloc::borrow::Cow;
525
526    use crate::{
527        param::IcalParam,
528        prop::{IcalProp, IcalPropKind, IcalPropName},
529        value::{IcalValue, text::IcalText},
530    };
531
532    #[test]
533    fn names_the_property_and_wraps_the_value() {
534        let prop = IcalProp {
535            name: IcalPropKind::Summary.into(),
536            params: [].into(),
537            value: IcalValue::Text(IcalText(Cow::Borrowed("Lunch"))),
538        };
539        assert_eq!(prop.name, IcalPropName::Kind(IcalPropKind::Summary));
540        assert_eq!(&*prop.name, "SUMMARY");
541        assert!(prop.params.is_empty());
542    }
543
544    #[test]
545    fn carries_the_given_parameters() {
546        let prop = IcalProp {
547            name: IcalPropKind::Attendee.into(),
548            params: [IcalParam::Role(Cow::Borrowed("CHAIR"))].into(),
549            value: IcalValue::CalAddress("mailto:a@b.example".into()),
550        };
551        assert_eq!(&*prop.name, "ATTENDEE");
552        assert_eq!(prop.params.len(), 1);
553    }
554
555    #[test]
556    fn round_trips_every_kind_through_its_wire_name() {
557        for kind in IcalPropKind::ALL {
558            assert_eq!(IcalPropKind::from_str(&kind).ok(), Some(kind));
559        }
560        // NOTE: Case-insensitive on the way in; unknown names are not in the
561        // vocabulary.
562        assert_eq!(
563            IcalPropKind::from_str("summary").ok(),
564            Some(IcalPropKind::Summary),
565        );
566        assert!(IcalPropKind::from_str("X-CUSTOM").is_err());
567    }
568}