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}