Skip to main content

ical/prop/
spec.rs

1//! # Property spec
2//!
3//! The per-property contract on the property markers, and the runtime vtable
4//! that bridges the open [`IcalPropKind`] back to those static impls.
5
6use crate::{
7    param::{COMMON_PARAMS, IcalParamKind},
8    prop::{
9        IcalPropKind, aalarm, acknowledged, action, attach, attendee, busytype, calendar_address,
10        calscale, cardinality::IcalPropCardinality, categories, class, color, comment, completed,
11        concept, conference, contact, created, dalarm, description, dtend, dtstamp, dtstart, due,
12        duration, exdate, exrule, freebusy, geo, image, last_modified, link, location,
13        location_type, malarm, method, name, organizer, palarm, participant_type, percent_complete,
14        priority, prodid, proximity, rdate, recurrence_id, refid, refresh_interval, related_to,
15        repeat, request_status, resource_type, resources, rnum, rrule, sequence, source, status,
16        structured_data, styled_description, summary, transp, trigger, tz, tzid, tzname,
17        tzoffsetfrom, tzoffsetto, tzurl, uid, url,
18    },
19    value::IcalValueKind,
20    version::IcalVersion,
21};
22
23/// The per-property contract: the versions it lives in, its multiplicity, the
24/// value types and parameters it may carry, all per version.
25///
26/// Implemented on the zero-sized property markers. The defaults cover the
27/// uniform majority (a single text value, valid in every version), so a
28/// property overrides only where it diverges; the only required item is
29/// [`KIND`](Self::KIND).
30pub trait IcalPropSpec {
31    /// The property this spec describes.
32    const KIND: IcalPropKind;
33
34    /// The versions in which the property is defined (the existence axis).
35    fn allowed_versions() -> &'static [IcalVersion] {
36        &[IcalVersion::V1_0, IcalVersion::V2_0]
37    }
38
39    /// How many times the property may appear in its component, in the given
40    /// version. Most are repeatable; the single-valued ones override this.
41    fn cardinality(_version: IcalVersion) -> IcalPropCardinality {
42        IcalPropCardinality::Any
43    }
44
45    /// The value-types the property may take, default-first, for the version.
46    /// Index 0 is the type used when no `VALUE` is declared.
47    fn allowed_values(_version: IcalVersion) -> &'static [IcalValueKind] {
48        &[IcalValueKind::Text]
49    }
50
51    /// The parameters the property may carry, in the given version.
52    fn allowed_params(_version: IcalVersion) -> &'static [IcalParamKind] {
53        COMMON_PARAMS
54    }
55
56    /// The value-type in force: the declared `VALUE` kind if any, else the
57    /// version default, else [`Text`](IcalValueKind::Text). Liberal: a declared
58    /// kind outside `allowed_values` is honoured here.
59    fn value(version: IcalVersion, declared: Option<IcalValueKind>) -> IcalValueKind {
60        declared
61            .or_else(|| Self::allowed_values(version).first().copied())
62            .unwrap_or(IcalValueKind::Text)
63    }
64}
65
66/// The spec of a property as function pointers, the runtime bridge from the
67/// open [`IcalPropKind`] back to the static per-marker [`IcalPropSpec`] impls.
68#[allow(dead_code)]
69pub(crate) struct IcalPropSpecFns {
70    /// The property this spec describes, so the dispatch can be checked against
71    /// itself.
72    pub kind: IcalPropKind,
73    /// See [`IcalPropSpec::allowed_versions`].
74    pub allowed_versions: fn() -> &'static [IcalVersion],
75    /// See [`IcalPropSpec::cardinality`].
76    pub cardinality: fn(IcalVersion) -> IcalPropCardinality,
77    /// See [`IcalPropSpec::allowed_values`].
78    pub allowed_values: fn(IcalVersion) -> &'static [IcalValueKind],
79    /// See [`IcalPropSpec::allowed_params`].
80    pub allowed_params: fn(IcalVersion) -> &'static [IcalParamKind],
81    /// See [`IcalPropSpec::value`].
82    pub value: fn(IcalVersion, Option<IcalValueKind>) -> IcalValueKind,
83}
84
85/// Collect the spec function pointers of a marker type.
86fn spec_fns<L: IcalPropSpec>() -> IcalPropSpecFns {
87    IcalPropSpecFns {
88        kind: L::KIND,
89        allowed_versions: L::allowed_versions,
90        cardinality: L::cardinality,
91        allowed_values: L::allowed_values,
92        allowed_params: L::allowed_params,
93        value: L::value,
94    }
95}
96
97/// Dispatch a property kind onto its marker spec.
98pub(crate) fn prop_spec(prop: IcalPropKind) -> IcalPropSpecFns {
99    use IcalPropKind::*;
100
101    match prop {
102        CalScale => spec_fns::<calscale::CALSCALE>(),
103        Method => spec_fns::<method::METHOD>(),
104        ProdId => spec_fns::<prodid::PRODID>(),
105        Attach => spec_fns::<attach::ATTACH>(),
106        Categories => spec_fns::<categories::CATEGORIES>(),
107        Class => spec_fns::<class::CLASS>(),
108        Comment => spec_fns::<comment::COMMENT>(),
109        Description => spec_fns::<description::DESCRIPTION>(),
110        Geo => spec_fns::<geo::GEO>(),
111        Location => spec_fns::<location::LOCATION>(),
112        PercentComplete => spec_fns::<percent_complete::PERCENT_COMPLETE>(),
113        Priority => spec_fns::<priority::PRIORITY>(),
114        Resources => spec_fns::<resources::RESOURCES>(),
115        Status => spec_fns::<status::STATUS>(),
116        Summary => spec_fns::<summary::SUMMARY>(),
117        Completed => spec_fns::<completed::COMPLETED>(),
118        DtEnd => spec_fns::<dtend::DTEND>(),
119        Due => spec_fns::<due::DUE>(),
120        DtStart => spec_fns::<dtstart::DTSTART>(),
121        Duration => spec_fns::<duration::DURATION>(),
122        FreeBusy => spec_fns::<freebusy::FREEBUSY>(),
123        Transp => spec_fns::<transp::TRANSP>(),
124        TzId => spec_fns::<tzid::TZID>(),
125        TzName => spec_fns::<tzname::TZNAME>(),
126        TzOffsetFrom => spec_fns::<tzoffsetfrom::TZOFFSETFROM>(),
127        TzOffsetTo => spec_fns::<tzoffsetto::TZOFFSETTO>(),
128        TzUrl => spec_fns::<tzurl::TZURL>(),
129        Attendee => spec_fns::<attendee::ATTENDEE>(),
130        Contact => spec_fns::<contact::CONTACT>(),
131        Organizer => spec_fns::<organizer::ORGANIZER>(),
132        RecurrenceId => spec_fns::<recurrence_id::RECURRENCE_ID>(),
133        RelatedTo => spec_fns::<related_to::RELATED_TO>(),
134        Url => spec_fns::<url::URL>(),
135        Uid => spec_fns::<uid::UID>(),
136        ExDate => spec_fns::<exdate::EXDATE>(),
137        RDate => spec_fns::<rdate::RDATE>(),
138        RRule => spec_fns::<rrule::RRULE>(),
139        ExRule => spec_fns::<exrule::EXRULE>(),
140        Action => spec_fns::<action::ACTION>(),
141        Repeat => spec_fns::<repeat::REPEAT>(),
142        Trigger => spec_fns::<trigger::TRIGGER>(),
143        Created => spec_fns::<created::CREATED>(),
144        DtStamp => spec_fns::<dtstamp::DTSTAMP>(),
145        LastModified => spec_fns::<last_modified::LAST_MODIFIED>(),
146        Sequence => spec_fns::<sequence::SEQUENCE>(),
147        RequestStatus => spec_fns::<request_status::REQUEST_STATUS>(),
148        Name => spec_fns::<name::NAME>(),
149        RefreshInterval => spec_fns::<refresh_interval::REFRESH_INTERVAL>(),
150        Source => spec_fns::<source::SOURCE>(),
151        Color => spec_fns::<color::COLOR>(),
152        Image => spec_fns::<image::IMAGE>(),
153        Conference => spec_fns::<conference::CONFERENCE>(),
154        ParticipantType => spec_fns::<participant_type::PARTICIPANT_TYPE>(),
155        ResourceType => spec_fns::<resource_type::RESOURCE_TYPE>(),
156        CalendarAddress => spec_fns::<calendar_address::CALENDAR_ADDRESS>(),
157        LocationType => spec_fns::<location_type::LOCATION_TYPE>(),
158        StructuredData => spec_fns::<structured_data::STRUCTURED_DATA>(),
159        Link => spec_fns::<link::LINK>(),
160        Refid => spec_fns::<refid::REFID>(),
161        Concept => spec_fns::<concept::CONCEPT>(),
162        BusyType => spec_fns::<busytype::BUSYTYPE>(),
163        StyledDescription => spec_fns::<styled_description::STYLED_DESCRIPTION>(),
164        Acknowledged => spec_fns::<acknowledged::ACKNOWLEDGED>(),
165        Proximity => spec_fns::<proximity::PROXIMITY>(),
166        Tz => spec_fns::<tz::TZ>(),
167        AAlarm => spec_fns::<aalarm::AALARM>(),
168        DAlarm => spec_fns::<dalarm::DALARM>(),
169        MAlarm => spec_fns::<malarm::MALARM>(),
170        PAlarm => spec_fns::<palarm::PALARM>(),
171        RNum => spec_fns::<rnum::RNUM>(),
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use crate::{
178        prop::{IcalPropKind, spec::prop_spec},
179        value::IcalValueKind,
180        version::IcalVersion,
181    };
182
183    #[test]
184    fn dispatches_every_property_onto_its_own_marker() {
185        for kind in IcalPropKind::ALL {
186            assert_eq!(prop_spec(kind).kind, kind, "{}", &*kind);
187        }
188    }
189
190    #[test]
191    fn every_property_states_a_value_kind_it_allows() {
192        for kind in IcalPropKind::ALL {
193            let spec = prop_spec(kind);
194
195            for version in (spec.allowed_versions)() {
196                let allowed = (spec.allowed_values)(*version);
197                let in_force = (spec.value)(*version, None);
198
199                // NOTE: With nothing declared, the kind in force is the first
200                // allowed one, so an empty allowed set would make the decoder
201                // fall back to text behind the spec's back.
202                assert!(!allowed.is_empty(), "{} allows no value kind", &*kind);
203                assert!(
204                    allowed.contains(&in_force),
205                    "{} decodes as {} which it does not allow",
206                    &*kind,
207                    &*in_force,
208                );
209            }
210        }
211    }
212
213    #[test]
214    fn a_declared_kind_wins_over_the_default() {
215        let spec = prop_spec(IcalPropKind::Attach);
216
217        assert_eq!(
218            (spec.value)(IcalVersion::V2_0, None),
219            IcalValueKind::Uri,
220            "the default is the first allowed kind"
221        );
222        assert_eq!(
223            (spec.value)(IcalVersion::V2_0, Some(IcalValueKind::Binary)),
224            IcalValueKind::Binary,
225            "a declared kind is honoured even outside the allowed set"
226        );
227    }
228
229    #[test]
230    fn a_list_property_stays_a_list_whatever_is_declared() {
231        let spec = prop_spec(IcalPropKind::RDate);
232
233        // NOTE: The declared kind describes each item, not the value as a
234        // whole, so RDATE;VALUE=PERIOD is still a list of periods.
235        assert_eq!(
236            (spec.value)(IcalVersion::V2_0, Some(IcalValueKind::Period)),
237            IcalValueKind::DateTimeList
238        );
239    }
240}