Skip to main content

ical/
prop.rs

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