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