use core::marker::PhantomData;
use alloc::vec::Vec;
use crate::{
param::VcardParam,
prop::{VcardProp, VcardPropName},
tree::{
prop::spec::VcardPropSpec,
vcard::validate::{VcardValid, VcardValidateError, validate_prop},
},
value::VcardValue,
vcard::Vcard,
version::VcardVersion,
};
pub struct VcardBuilder<'a> {
pub version: VcardVersion,
pub properties: Vec<VcardProp<'a>>,
}
impl<'a> VcardBuilder<'a> {
pub fn new(version: VcardVersion) -> Self {
Self {
version,
properties: Vec::new(),
}
}
pub fn prop<L: VcardPropSpec>(self) -> VcardPropInProgress<'a, L> {
let inner = VcardPropBuilder::new(self.version);
VcardPropInProgress { card: self, inner }
}
pub fn build(self) -> Result<VcardValid<Vcard<'a>>, Vec<VcardValidateError>> {
self.build_unchecked().validate()
}
pub fn build_unchecked(self) -> Vcard<'a> {
Vcard {
version: self.version,
properties: self.properties,
}
}
}
pub struct VcardPropInProgress<'a, L: VcardPropSpec> {
card: VcardBuilder<'a>,
inner: VcardPropBuilder<'a, L>,
}
impl<'a, L: VcardPropSpec> VcardPropInProgress<'a, L> {
pub fn param(mut self, param: VcardParam<'a>) -> Self {
self.inner = self.inner.param(param);
self
}
pub fn value(mut self, value: VcardValue<'a>) -> VcardBuilder<'a> {
self.card.properties.push(VcardProp {
name: VcardPropName::Kind(L::KIND),
params: self.inner.params,
value,
});
self.card
}
}
pub struct VcardPropBuilder<'a, L: VcardPropSpec> {
pub version: VcardVersion,
pub params: Vec<VcardParam<'a>>,
lens: PhantomData<L>,
}
impl<'a, L: VcardPropSpec> VcardPropBuilder<'a, L> {
pub fn new(version: VcardVersion) -> Self {
Self {
version,
params: Vec::new(),
lens: PhantomData,
}
}
pub fn param(mut self, param: VcardParam<'a>) -> Self {
self.params.push(param);
self
}
pub fn build(self, value: VcardValue<'a>) -> Result<VcardProp<'a>, Vec<VcardValidateError>> {
let prop = VcardProp {
name: VcardPropName::Kind(L::KIND),
params: self.params,
value,
};
let mut errors = Vec::new();
validate_prop(&prop, self.version, &mut errors);
if errors.is_empty() {
Ok(prop)
} else {
Err(errors)
}
}
}
#[cfg(test)]
mod tests {
use alloc::{borrow::Cow, vec};
use crate::{
param::VcardParam,
tree::{prop::r#fn::FN, vcard::builder::VcardPropBuilder},
value::{VcardValue, text::VcardText},
version::VcardVersion,
};
#[test]
fn builds_a_property_pinning_the_name_from_the_spec() {
let prop = VcardPropBuilder::<FN>::new(VcardVersion::V4_0)
.param(VcardParam::Pref(Cow::Borrowed("1")))
.build(VcardValue::Text(VcardText(Cow::Borrowed("John"))))
.expect("FN takes text with a PREF param");
assert_eq!(&*prop.name, "FN");
assert_eq!(prop.params, vec![VcardParam::Pref(Cow::Borrowed("1"))]);
assert_eq!(
prop.value,
VcardValue::Text(VcardText(Cow::Borrowed("John"))),
);
}
#[test]
fn refuses_a_value_kind_the_property_does_not_allow() {
let built = VcardPropBuilder::<FN>::new(VcardVersion::V4_0).build(VcardValue::Uri(
crate::value::uri::VcardUri(Cow::Borrowed("x")),
));
assert!(built.is_err());
}
#[test]
fn refuses_a_param_the_property_does_not_allow() {
let built = VcardPropBuilder::<FN>::new(VcardVersion::V4_0)
.param(VcardParam::MediaType(Cow::Borrowed("text/plain")))
.build(VcardValue::Text(VcardText(Cow::Borrowed("John"))));
assert!(built.is_err());
}
#[test]
fn card_builder_chains_props_and_validates() {
use crate::tree::{prop::note::NOTE, vcard::builder::VcardBuilder};
let valid = VcardBuilder::new(VcardVersion::V4_0)
.prop::<FN>()
.param(VcardParam::Pref(Cow::Borrowed("1")))
.value(VcardValue::Text(VcardText(Cow::Borrowed("John Doe"))))
.prop::<NOTE>()
.value(VcardValue::Text(VcardText(Cow::Borrowed("a note"))))
.build()
.expect("a conformant 4.0 card");
assert_eq!(valid.properties.len(), 2);
assert_eq!(&*valid.properties[0].name, "FN");
assert_eq!(&*valid.properties[1].name, "NOTE");
}
#[test]
fn card_builder_defers_a_violation_to_build() {
use crate::tree::vcard::builder::VcardBuilder;
let built = VcardBuilder::new(VcardVersion::V4_0)
.prop::<FN>()
.param(VcardParam::MediaType(Cow::Borrowed("text/plain")))
.value(VcardValue::Text(VcardText(Cow::Borrowed("John"))))
.build();
assert!(built.is_err());
}
}