Skip to main content

ical/tree/prop/
spec.rs

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