Skip to main content

ical/
builder.rs

1//! # Property builder
2//!
3//! Strict, version-aware construction of a single property.
4//!
5//! [`IcalPropBuilder`] is the write-side counterpart of the lenses: keyed by
6//! the same zero-sized property markers, it carries the calendar version,
7//! accumulates parameters, and emits an open [`IcalProp`].
8//!
9//! Its name is pinned by the marker's [`IcalPropSpec`], and
10//! [`build`](IcalPropBuilder::build) runs the shared per-property check
11//! ([`validate_prop`](crate::validator)), so a known property must
12//! be defined in the calendar's version (extensions pass).
13//!
14//! To emit something the spec forbids, construct the open [`IcalProp`] by
15//! hand. The version is a value the builder carries, never a type parameter.
16//!
17//! # Example
18//!
19//! ```rust
20//! use ical::builder::IcalPropBuilder;
21//! use ical::prop::summary::SUMMARY;
22//! use ical::param::IcalParam;
23//! use ical::value::IcalValue;
24//! use ical::value::text::IcalText;
25//! use ical::version::IcalVersion;
26//! use std::borrow::Cow;
27//!
28//! let prop = IcalPropBuilder::<SUMMARY>::new(IcalVersion::V2_0)
29//!     .param(IcalParam::Language(Cow::Borrowed("en")))
30//!     .build(IcalValue::Text(IcalText(Cow::Borrowed("Lunch"))))
31//!     .expect("SUMMARY accepts text with a LANGUAGE parameter");
32//!
33//! assert_eq!(&*prop.name, "SUMMARY");
34//! ```
35
36use core::marker::PhantomData;
37
38use alloc::vec::Vec;
39
40use crate::{
41    param::IcalParam,
42    prop::{IcalProp, IcalPropName, spec::IcalPropSpec},
43    validator::{IcalValidateError, validate_prop},
44    value::IcalValue,
45    version::IcalVersion,
46};
47
48/// A version-aware builder for one property, keyed by its property marker.
49pub struct IcalPropBuilder<'a, L: IcalPropSpec> {
50    /// The calendar version the property is built for.
51    pub version: IcalVersion,
52    /// The parameters accumulated so far.
53    pub params: Vec<IcalParam<'a>>,
54    lens: PhantomData<L>,
55}
56
57impl<'a, L: IcalPropSpec> IcalPropBuilder<'a, L> {
58    /// Start a builder for the given calendar version.
59    pub fn new(version: IcalVersion) -> Self {
60        Self {
61            version,
62            params: Vec::new(),
63            lens: PhantomData,
64        }
65    }
66
67    /// Add a parameter (validated against the spec on [`build`](Self::build)).
68    pub fn param(mut self, param: IcalParam<'a>) -> Self {
69        self.params.push(param);
70        self
71    }
72
73    /// Finish with a value, emitting the property named by the spec.
74    ///
75    /// Runs the same per-property check as
76    /// [`Ical::validate`](crate::ical::Ical::validate): the value kind must be
77    /// allowed and every known parameter must be allowed for the version
78    /// (unknown, i.e. extension, parameters pass).
79    pub fn build(self, value: IcalValue<'a>) -> Result<IcalProp<'a>, Vec<IcalValidateError>> {
80        let prop = IcalProp {
81            name: IcalPropName::Kind(L::KIND),
82            params: self.params,
83            value,
84        };
85
86        let mut errors = Vec::new();
87
88        validate_prop(&prop, self.version, &mut errors);
89
90        if errors.is_empty() {
91            Ok(prop)
92        } else {
93            Err(errors)
94        }
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use alloc::{borrow::Cow, vec};
101
102    use crate::{
103        builder::IcalPropBuilder,
104        param::IcalParam,
105        prop::summary::SUMMARY,
106        value::{IcalValue, text::IcalText},
107        version::IcalVersion,
108    };
109
110    #[test]
111    fn builds_a_property_pinning_the_name_from_the_spec() {
112        let prop = IcalPropBuilder::<SUMMARY>::new(IcalVersion::V2_0)
113            .param(IcalParam::Language(Cow::Borrowed("en")))
114            .build(IcalValue::Text(IcalText(Cow::Borrowed("Lunch"))))
115            .expect("SUMMARY takes text with a LANGUAGE param");
116
117        assert_eq!(&*prop.name, "SUMMARY");
118        assert_eq!(prop.params, vec![IcalParam::Language(Cow::Borrowed("en"))]);
119        assert_eq!(
120            prop.value,
121            IcalValue::Text(IcalText(Cow::Borrowed("Lunch")))
122        );
123    }
124}