Skip to main content

vcard/
validator.rs

1//! # Validator
2//!
3//! The strict half of "liberal in, strict out", as a runtime predicate rather
4//! than a second data model.
5//!
6//! Validity and lossiness are orthogonal: a conformant card may still carry
7//! extensions (`X-`/IANA properties, unknown parameters), so "valid" cannot be
8//! a type with no `Unknown` arms.
9//!
10//! [`Vcard::validate`] therefore checks the *known* parts of the (lossy) model
11//! against the per-property
12//! [`VcardPropSpec`](crate::prop::spec::VcardPropSpec) for the card
13//! version, and leaves the unknown parts alone.
14//!
15//! The check covers existence, value kind, parameters and cardinality, which
16//! are a property's shape, plus the content of the few values whose RFC closes
17//! it: `GENDER`'s sex code, `PROFILE`'s single value, `CLIENTPIDMAP`'s
18//! identifier, and the `PREF`, `PID` and `DERIVED` parameters.
19//!
20//! Content stops there. A vocabulary ending in `iana-token / x-name` is open
21//! and nothing to check against, and a date, a URI or a language tag is a
22//! grammar rather than a set: reading those is a different appetite, and one
23//! that would have this crate carrying a URI parser to answer a question no
24//! caller asked.
25//!
26//! A passing check mints a [`VcardValid`] marker, the only way to obtain one,
27//! so holding a `VcardValid<Vcard>` is proof the check passed. The same
28//! per-property check backs the [`VcardPropBuilder`]'s strict construction.
29//!
30//! [`VcardPropBuilder`]: crate::builder::VcardPropBuilder
31//!
32//! ## Example
33//!
34//! Decode a parsed card, validate it into a proof, and convert that back into a
35//! byte tree:
36//!
37//! ```rust
38//! # #[cfg(feature = "parser")] {
39//! use vcard::tree::cst::VcardCst;
40//!
41//! let raw = "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:John Doe\r\nEND:VCARD\r\n";
42//! let cst = VcardCst::parse(raw).unwrap();
43//!
44//! // validate consumes the card and returns the proof (or the violations).
45//! let valid = cst.decode().validate().expect("a conformant 4.0 card");
46//!
47//! // The proof converts back into a byte tree for free.
48//! let out = VcardCst::from(valid);
49//! assert!(out.to_string().contains("FN:John Doe"));
50//! # }
51//! ```
52
53use core::{error, fmt, ops::Deref};
54
55use alloc::{
56    string::{String, ToString},
57    vec,
58    vec::Vec,
59};
60
61use crate::{
62    param::{VcardParam, VcardParamKind},
63    prop::{
64        VcardProp, VcardPropKind, VcardPropName,
65        cardinality::VcardPropCardinality,
66        spec::{VcardPropSpecFns, prop_spec},
67    },
68    value::VcardValueKind,
69    vcard::Vcard,
70    version::VcardVersion,
71};
72
73/// A way a known property breaks its RFC 6350 contract for the card version.
74#[derive(Clone, Debug, PartialEq, Eq)]
75pub enum VcardValidateError {
76    /// The property is not defined in this version.
77    PropVersion {
78        /// The offending property.
79        prop: VcardPropKind,
80        /// The card version.
81        version: VcardVersion,
82    },
83    /// The value kind is not allowed for the property (a `None` is an undecoded
84    /// value on a known property).
85    ValueKind {
86        /// The offending property.
87        prop: VcardPropKind,
88        /// The value kind found, if any.
89        found: Option<VcardValueKind>,
90    },
91    /// The parameter is not allowed for the property in this version.
92    Param {
93        /// The offending property.
94        prop: VcardPropKind,
95        /// The disallowed parameter.
96        param: VcardParamKind,
97    },
98    /// The value holds something the property's own definition forbids.
99    ///
100    /// Only the few properties whose RFC closes their content raise this:
101    /// `GENDER`'s sex code, `PROFILE`'s single value, `CLIENTPIDMAP`'s
102    /// identifier. A vocabulary ending in `iana-token / x-name` is open,
103    /// and nothing to check against.
104    Value {
105        /// The offending property.
106        prop: VcardPropKind,
107        /// The value found, as the card wrote it.
108        found: String,
109    },
110    /// The parameter's value is outside what the parameter accepts.
111    ///
112    /// `PREF` outside 1 to 100, a `PID` that is not a small integer or a
113    /// pair of them, a `DERIVED` that is neither `true` nor `false`.
114    ParamValue {
115        /// The offending parameter.
116        param: VcardParamKind,
117        /// The value found, as the card wrote it.
118        found: String,
119    },
120    /// The property appears a number of times its multiplicity forbids.
121    Cardinality {
122        /// The offending property.
123        prop: VcardPropKind,
124        /// The required multiplicity.
125        cardinality: VcardPropCardinality,
126        /// How many times it actually appears.
127        count: usize,
128    },
129}
130
131impl fmt::Display for VcardValidateError {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        match self {
134            Self::PropVersion {
135                prop: p,
136                version: v,
137            } => {
138                write!(f, "Property `{}` is not defined in vCard {}", &**p, &**v)
139            }
140            Self::ValueKind {
141                prop: p,
142                found: Some(k),
143            } => {
144                write!(f, "Value kind `{}` is not allowed for `{}`", &**k, &**p)
145            }
146            Self::ValueKind {
147                prop: p,
148                found: None,
149            } => {
150                write!(f, "An undecoded value is not allowed for `{}`", &**p)
151            }
152            Self::Param {
153                prop: pp,
154                param: pm,
155            } => {
156                write!(f, "Parameter `{}` is not allowed for `{}`", &**pm, &**pp)
157            }
158            Self::Value { prop: p, found } => {
159                write!(f, "Value `{found}` is not allowed for `{}`", &**p)
160            }
161            Self::ParamValue { param: p, found } => {
162                write!(f, "Value `{found}` is not allowed for parameter `{}`", &**p)
163            }
164            Self::Cardinality {
165                prop: p,
166                cardinality: cd,
167                count: cn,
168            } => {
169                write!(f, "Property `{}` appears {cn} times but is {cd:?}", &**p)
170            }
171        }
172    }
173}
174
175impl error::Error for VcardValidateError {}
176
177impl<'a> Vcard<'a> {
178    /// Check the card against RFC 6350 for its version.
179    ///
180    /// Every known property must exist in the version, carry an allowed value
181    /// kind and parameters, and respect its multiplicity; extensions pass. The
182    /// card comes back as a [`VcardValid`] proof, or every violation does.
183    pub fn validate(self) -> Result<VcardValid<Vcard<'a>>, Vec<VcardValidateError>> {
184        let mut errors = Vec::new();
185        let mut counts: Vec<(VcardPropKind, usize)> = Vec::new();
186
187        for prop in &self.properties {
188            validate_prop(prop, self.version, &mut errors);
189            if let VcardPropName::Kind(kind) = &prop.name {
190                match counts.iter_mut().find(|(seen, _)| *seen == *kind) {
191                    Some((_, count)) => *count += 1,
192                    None => counts.push((*kind, 1)),
193                }
194            }
195        }
196
197        // NOTE: scan every kind defined in this version, so a required property
198        // that is absent (count 0) is caught alongside one that appears too
199        // often.
200        for prop in VcardPropKind::ALL {
201            let spec = prop_spec(prop);
202            if !(spec.allowed_versions)().contains(&self.version) {
203                continue;
204            }
205            let count = counts
206                .iter()
207                .find(|(seen, _)| *seen == prop)
208                .map_or(0, |(_, count)| *count);
209            let cardinality = (spec.cardinality)(self.version);
210            if !cardinality_ok(cardinality, count) {
211                errors.push(VcardValidateError::Cardinality {
212                    prop,
213                    cardinality,
214                    count,
215                });
216            }
217        }
218
219        if errors.is_empty() {
220            Ok(VcardValid(self))
221        } else {
222            Err(errors)
223        }
224    }
225}
226
227/// Check one property against its spec for the version, pushing any violations.
228/// An unknown (extension) property is always conformant. Shared by
229/// [`Vcard::validate`] and the
230/// [`VcardPropBuilder`](crate::builder::VcardPropBuilder).
231pub(crate) fn validate_prop(
232    prop: &VcardProp<'_>,
233    version: VcardVersion,
234    errors: &mut Vec<VcardValidateError>,
235) {
236    let VcardPropName::Kind(kind) = &prop.name else {
237        return;
238    };
239
240    let spec = prop_spec(*kind);
241
242    if !(spec.allowed_versions)().contains(&version) {
243        errors.push(VcardValidateError::PropVersion {
244            prop: *kind,
245            version,
246        });
247    }
248
249    let value_kind = prop.value.kind();
250    if !value_kind.is_some_and(|kind| (spec.allowed_values)(version).contains(&kind)) {
251        errors.push(VcardValidateError::ValueKind {
252            prop: *kind,
253            found: value_kind,
254        });
255    }
256
257    if let Some(found) = (spec.invalid_value)(&prop.value, version) {
258        errors.push(VcardValidateError::Value { prop: *kind, found });
259    }
260
261    for param in &prop.params {
262        let Some(param_kind) = param.kind() else {
263            continue;
264        };
265
266        if !param_allowed(&spec, version, param_kind) {
267            errors.push(VcardValidateError::Param {
268                prop: *kind,
269                param: param_kind,
270            });
271        }
272
273        for found in invalid_param_values(param) {
274            errors.push(VcardValidateError::ParamValue {
275                param: param_kind,
276                found,
277            });
278        }
279    }
280}
281
282/// The values a parameter carries that its own definition forbids.
283///
284/// These constraints do not vary by the property carrying the parameter, so
285/// one check serves every appearance rather than each property restating it.
286///
287/// `PREF` is an integer from 1 to 100 (RFC 6350 5.3), `PID` one or more
288/// digits optionally followed by a dot and more digits (5.5), and `DERIVED`
289/// either `true` or `false` (RFC 9554 3.4). Every other parameter is free
290/// text, a language tag, a media type or an open vocabulary, and none of
291/// those is a set to check against.
292fn invalid_param_values(param: &VcardParam<'_>) -> Vec<String> {
293    match param {
294        VcardParam::Pref(value) => {
295            let pref = value.parse::<u8>();
296            offending(pref.is_ok_and(|pref| (1..=100).contains(&pref)), value)
297        }
298        VcardParam::Derived(value) => {
299            let boolean = value.eq_ignore_ascii_case("true") || value.eq_ignore_ascii_case("false");
300            offending(boolean, value)
301        }
302        VcardParam::Pid(values) => values
303            .iter()
304            .filter(|value| !is_pid(value))
305            .map(|value| value.to_string())
306            .collect(),
307        _ => Vec::new(),
308    }
309}
310
311/// A single-valued parameter's value when it is not allowed, nothing when it
312/// is.
313fn offending(allowed: bool, value: &str) -> Vec<String> {
314    match allowed {
315        true => Vec::new(),
316        false => vec![value.to_string()],
317    }
318}
319
320/// Whether a `PID` value is `1*DIGIT ["." 1*DIGIT]` (RFC 6350 5.5).
321fn is_pid(value: &str) -> bool {
322    let digits = |part: &str| !part.is_empty() && part.bytes().all(|byte| byte.is_ascii_digit());
323
324    match value.split_once('.') {
325        Some((source, client)) => digits(source) && digits(client),
326        None => digits(value),
327    }
328}
329
330/// Whether a known parameter is allowed on a property in a version. 4.0 uses
331/// the spec's set directly; 2.1 / 3.0 drop the parameters introduced in 4.0 and
332/// allow the legacy `ENCODING` / `CHARSET`.
333fn param_allowed(spec: &VcardPropSpecFns, version: VcardVersion, kind: VcardParamKind) -> bool {
334    let allowed = (spec.allowed_params)(version);
335    match version {
336        VcardVersion::V4_0 => allowed.contains(&kind) || is_universal(kind),
337        _ => {
338            matches!(kind, VcardParamKind::Charset | VcardParamKind::Encoding)
339                || (allowed.contains(&kind) && !is_v4_only(kind))
340        }
341    }
342}
343
344/// Whether an RFC 9554 parameter is defined for any property, so 4.0 allows
345/// it without each spec listing it.
346fn is_universal(kind: VcardParamKind) -> bool {
347    use VcardParamKind::*;
348
349    matches!(kind, Author | AuthorName | Created | Derived | PropId)
350}
351
352/// Whether a parameter was introduced in vCard 4.0 or later (so 2.1 / 3.0
353/// disallow it).
354fn is_v4_only(kind: VcardParamKind) -> bool {
355    use VcardParamKind::*;
356
357    matches!(
358        kind,
359        Pid | Pref
360            | AltId
361            | MediaType
362            | CalScale
363            | SortAs
364            | Geo
365            | Tz
366            | Label
367            | Author
368            | AuthorName
369            | Created
370            | Derived
371            | Jsptr
372            | Phonetic
373            | PropId
374            | Script
375            | ServiceType
376            | Username
377    )
378}
379
380/// Whether `count` occurrences satisfy the multiplicity.
381fn cardinality_ok(cardinality: VcardPropCardinality, count: usize) -> bool {
382    match cardinality {
383        VcardPropCardinality::ExactlyOne => count == 1,
384        VcardPropCardinality::AtMostOne => count <= 1,
385        VcardPropCardinality::OneOrMore => count >= 1,
386        VcardPropCardinality::Any => true,
387    }
388}
389
390/// A value that has passed validation.
391///
392/// The only way to mint one is a validating conversion ([`Vcard::validate`] or
393/// its `TryFrom`), so holding a `VcardValid<T>` is proof the check passed. It
394/// derefs for reads and yields the value with [`into_inner`](Self::into_inner).
395#[derive(Clone, Debug, PartialEq, Eq)]
396pub struct VcardValid<T>(T);
397
398impl<T> VcardValid<T> {
399    /// Take the validated value back out.
400    pub fn into_inner(self) -> T {
401        self.0
402    }
403}
404
405impl<T> Deref for VcardValid<T> {
406    type Target = T;
407
408    fn deref(&self) -> &T {
409        &self.0
410    }
411}
412
413impl<'a> TryFrom<Vcard<'a>> for VcardValid<Vcard<'a>> {
414    type Error = Vec<VcardValidateError>;
415
416    fn try_from(card: Vcard<'a>) -> Result<Self, Self::Error> {
417        card.validate()
418    }
419}
420
421#[cfg(test)]
422mod tests {
423    use alloc::{borrow::Cow, string::ToString, vec, vec::Vec};
424
425    use crate::{
426        param::{VcardParam, VcardParamKind},
427        prop::{VcardProp, VcardPropKind, cardinality::VcardPropCardinality},
428        validator::{VcardValid, VcardValidateError},
429        value::{
430            VcardValue, VcardValueKind, client_pid_map::VcardClientPidMap, gender::VcardGender,
431            n::VcardN, text::VcardText, uri::VcardUri,
432        },
433        vcard::Vcard,
434        version::VcardVersion,
435    };
436
437    fn prop(
438        name: &'static str,
439        params: Vec<VcardParam<'static>>,
440        value: VcardValue<'static>,
441    ) -> VcardProp<'static> {
442        VcardProp {
443            name: name.into(),
444            params,
445            value,
446        }
447    }
448
449    fn card(version: VcardVersion, properties: Vec<VcardProp<'static>>) -> Vcard<'static> {
450        Vcard {
451            version,
452            properties,
453        }
454    }
455
456    #[test]
457    fn accepts_a_conformant_card_and_extensions() {
458        let vcard = card(
459            VcardVersion::V4_0,
460            vec![
461                prop(
462                    "FN",
463                    vec![],
464                    VcardValue::Text(VcardText(Cow::Borrowed("John"))),
465                ),
466                // NOTE: An X- extension property is conformant.
467                prop("X-FOO", vec![], VcardValue::Unknown(Default::default())),
468            ],
469        );
470        assert!(vcard.validate().is_ok());
471    }
472
473    /// A 4.0 card carrying `FN` and one more property, the minimum that
474    /// passes cardinality.
475    fn card_with(other: VcardProp<'static>) -> Vcard<'static> {
476        card(
477            VcardVersion::V4_0,
478            vec![
479                prop(
480                    "FN",
481                    vec![],
482                    VcardValue::Text(VcardText(Cow::Borrowed("John"))),
483                ),
484                other,
485            ],
486        )
487    }
488
489    fn gender(sex: &'static str, identity: &'static str) -> VcardProp<'static> {
490        prop(
491            "GENDER",
492            vec![],
493            VcardValue::Gender(VcardGender {
494                sex: Cow::Borrowed(sex),
495                identity: Cow::Borrowed(identity),
496            }),
497        )
498    }
499
500    /// A property carrying one parameter, for the parameter checks.
501    fn with_param(param: VcardParam<'static>) -> VcardProp<'static> {
502        prop(
503            "NICKNAME",
504            vec![param],
505            VcardValue::TextList(Default::default()),
506        )
507    }
508
509    #[test]
510    fn accepts_every_sex_code_the_rfc_defines() {
511        // RFC 6350 6.2.7: sex = "" / "M" / "F" / "O" / "N" / "U", and RFC
512        // 5234 makes a quoted ABNF literal case-insensitive.
513        for sex in ["", "M", "F", "O", "N", "U", "m", "f"] {
514            assert!(
515                card_with(gender(sex, "")).validate().is_ok(),
516                "rejected the sex code {sex:?}",
517            );
518        }
519    }
520
521    #[test]
522    fn flags_a_sex_code_outside_the_vocabulary() {
523        let errors = card_with(gender("X", "")).validate().unwrap_err();
524
525        assert!(errors.contains(&VcardValidateError::Value {
526            prop: VcardPropKind::Gender,
527            found: "X".to_string(),
528        }));
529    }
530
531    #[test]
532    fn a_gender_outside_the_vocabulary_belongs_in_the_identity() {
533        // Which is what the RFC's own `GENDER:;it's complicated` does.
534        assert!(card_with(gender("", "it's complicated")).validate().is_ok());
535    }
536
537    #[test]
538    fn flags_a_profile_that_is_not_vcard() {
539        let profile = |value| {
540            card(
541                VcardVersion::V3_0,
542                vec![
543                    prop(
544                        "FN",
545                        vec![],
546                        VcardValue::Text(VcardText(Cow::Borrowed("John"))),
547                    ),
548                    prop("N", vec![], VcardValue::N(VcardN::default())),
549                    prop("PROFILE", vec![], VcardValue::Text(VcardText(value))),
550                ],
551            )
552        };
553
554        assert!(profile(Cow::Borrowed("VCARD")).validate().is_ok());
555        assert!(profile(Cow::Borrowed("vcard")).validate().is_ok());
556        assert!(profile(Cow::Borrowed("ICAL")).validate().is_err());
557    }
558
559    #[test]
560    fn flags_a_client_pid_map_identifier_that_is_not_an_integer() {
561        let map = |id| {
562            card_with(prop(
563                "CLIENTPIDMAP",
564                vec![],
565                VcardValue::ClientPidMap(VcardClientPidMap {
566                    id,
567                    uri: Cow::Borrowed("urn:uuid:1f"),
568                }),
569            ))
570        };
571
572        assert!(map(Cow::Borrowed("1")).validate().is_ok());
573        assert!(map(Cow::Borrowed("")).validate().is_err());
574        assert!(map(Cow::Borrowed("one")).validate().is_err());
575    }
576
577    #[test]
578    fn flags_a_pref_outside_one_to_a_hundred() {
579        let pref = |value| card_with(with_param(VcardParam::Pref(Cow::Borrowed(value))));
580
581        assert!(pref("1").validate().is_ok());
582        assert!(pref("100").validate().is_ok());
583        assert!(pref("0").validate().is_err());
584        assert!(pref("101").validate().is_err());
585        assert!(pref("high").validate().is_err());
586    }
587
588    #[test]
589    fn flags_a_pid_that_is_not_a_small_integer_pair() {
590        let pid = |values| card_with(with_param(VcardParam::Pid(values)));
591
592        assert!(pid(vec![Cow::Borrowed("1")]).validate().is_ok());
593        assert!(pid(vec![Cow::Borrowed("1.1")]).validate().is_ok());
594        assert!(pid(vec![Cow::Borrowed("1.")]).validate().is_err());
595        assert!(pid(vec![Cow::Borrowed("a")]).validate().is_err());
596    }
597
598    #[test]
599    fn flags_a_derived_that_is_not_a_boolean() {
600        let derived = |value| card_with(with_param(VcardParam::Derived(Cow::Borrowed(value))));
601
602        assert!(derived("true").validate().is_ok());
603        assert!(derived("FALSE").validate().is_ok());
604        assert!(derived("yes").validate().is_err());
605    }
606
607    #[test]
608    fn an_open_vocabulary_is_not_checked() {
609        // RFC 6350 6.1.4 ends KIND's grammar in `iana-token / x-name`, so a
610        // value outside the listed ones still conforms.
611        let kind = prop(
612            "KIND",
613            vec![],
614            VcardValue::Text(VcardText(Cow::Borrowed("x-android-custom"))),
615        );
616
617        assert!(card_with(kind).validate().is_ok());
618    }
619
620    #[test]
621    fn flags_a_value_kind_the_property_forbids() {
622        let vcard = card(
623            VcardVersion::V4_0,
624            vec![prop(
625                "FN",
626                vec![],
627                VcardValue::Uri(VcardUri(Cow::Borrowed("x"))),
628            )],
629        );
630        let errors = vcard.validate().unwrap_err();
631        assert!(matches!(errors[0], VcardValidateError::ValueKind { .. }));
632    }
633
634    /// N and FN make the envelope conformant in both versions, so only the
635    /// CHARSET parameter, legal in 2.1 but not in 4.0, decides the outcome.
636    #[test]
637    fn allows_charset_in_2_1_but_not_4_0() {
638        let with_charset = |version| {
639            card(
640                version,
641                vec![
642                    prop("N", vec![], VcardValue::N(VcardN::default())),
643                    prop(
644                        "FN",
645                        vec![],
646                        VcardValue::Text(VcardText(Cow::Borrowed("X"))),
647                    ),
648                    prop(
649                        "NOTE",
650                        vec![VcardParam::Charset(Cow::Borrowed("UTF-8"))],
651                        VcardValue::Text(VcardText(Cow::Borrowed("hi"))),
652                    ),
653                ],
654            )
655            .validate()
656        };
657
658        assert!(with_charset(VcardVersion::V2_1).is_ok());
659        assert!(with_charset(VcardVersion::V4_0).is_err());
660    }
661
662    /// 4.0 requires FN one or more times, so a card without it fails.
663    #[test]
664    fn flags_a_required_property_that_is_absent() {
665        let errors = card(VcardVersion::V4_0, vec![]).validate().unwrap_err();
666        assert!(errors.iter().any(|error| matches!(
667            error,
668            VcardValidateError::Cardinality {
669                prop: VcardPropKind::Fn,
670                ..
671            },
672        )));
673    }
674
675    /// AGENT is 2.1 and 3.0 only, so in 4.0 it is undefined.
676    #[test]
677    fn flags_a_property_absent_from_the_version() {
678        let errors = card(
679            VcardVersion::V4_0,
680            vec![
681                prop(
682                    "FN",
683                    vec![],
684                    VcardValue::Text(VcardText(Cow::Borrowed("X"))),
685                ),
686                prop(
687                    "AGENT",
688                    vec![],
689                    VcardValue::Text(VcardText(Cow::Borrowed("a"))),
690                ),
691            ],
692        )
693        .validate()
694        .unwrap_err();
695        assert!(errors.iter().any(|error| matches!(
696            error,
697            VcardValidateError::PropVersion {
698                prop: VcardPropKind::Agent,
699                ..
700            },
701        )));
702    }
703
704    /// FN is text-only and does not allow MEDIATYPE.
705    #[test]
706    fn flags_a_disallowed_parameter() {
707        let errors = card(
708            VcardVersion::V4_0,
709            vec![prop(
710                "FN",
711                vec![VcardParam::MediaType(Cow::Borrowed("text/plain"))],
712                VcardValue::Text(VcardText(Cow::Borrowed("X"))),
713            )],
714        )
715        .validate()
716        .unwrap_err();
717        assert!(
718            errors
719                .iter()
720                .any(|error| matches!(error, VcardValidateError::Param { .. },))
721        );
722    }
723
724    /// 4.0 makes N at most one, so two N properties are too many.
725    #[test]
726    fn flags_a_property_that_appears_too_often() {
727        let errors = card(
728            VcardVersion::V4_0,
729            vec![
730                prop(
731                    "FN",
732                    vec![],
733                    VcardValue::Text(VcardText(Cow::Borrowed("X"))),
734                ),
735                prop("N", vec![], VcardValue::N(VcardN::default())),
736                prop("N", vec![], VcardValue::N(VcardN::default())),
737            ],
738        )
739        .validate()
740        .unwrap_err();
741        assert!(errors.iter().any(|error| matches!(
742            error,
743            VcardValidateError::Cardinality {
744                prop: VcardPropKind::N,
745                count: 2,
746                ..
747            },
748        )));
749    }
750
751    /// PID is 4.0 only, so on a 2.1 property it is disallowed.
752    #[test]
753    fn rejects_a_v4_only_parameter_in_2_1() {
754        let errors = card(
755            VcardVersion::V2_1,
756            vec![
757                prop("N", vec![], VcardValue::N(VcardN::default())),
758                prop(
759                    "FN",
760                    vec![],
761                    VcardValue::Text(VcardText(Cow::Borrowed("X"))),
762                ),
763                prop(
764                    "TEL",
765                    vec![VcardParam::Pid(vec![Cow::Borrowed("1")])],
766                    VcardValue::Text(VcardText(Cow::Borrowed("123"))),
767                ),
768            ],
769        )
770        .validate()
771        .unwrap_err();
772        assert!(errors.iter().any(|error| matches!(
773            error,
774            VcardValidateError::Param {
775                param: VcardParamKind::Pid,
776                ..
777            },
778        )));
779    }
780
781    #[test]
782    fn displays_every_validate_error_variant() {
783        let errors = [
784            VcardValidateError::PropVersion {
785                prop: VcardPropKind::Agent,
786                version: VcardVersion::V4_0,
787            },
788            VcardValidateError::ValueKind {
789                prop: VcardPropKind::Fn,
790                found: Some(VcardValueKind::Uri),
791            },
792            VcardValidateError::ValueKind {
793                prop: VcardPropKind::Fn,
794                found: None,
795            },
796            VcardValidateError::Param {
797                prop: VcardPropKind::Fn,
798                param: VcardParamKind::MediaType,
799            },
800            VcardValidateError::Cardinality {
801                prop: VcardPropKind::N,
802                cardinality: VcardPropCardinality::AtMostOne,
803                count: 2,
804            },
805        ];
806        for error in errors {
807            assert!(!error.to_string().is_empty());
808        }
809    }
810
811    #[test]
812    fn valid_proof_derefs_and_unwraps() {
813        let vcard = card(
814            VcardVersion::V4_0,
815            vec![prop(
816                "FN",
817                vec![],
818                VcardValue::Text(VcardText(Cow::Borrowed("John"))),
819            )],
820        );
821
822        let valid = VcardValid::try_from(vcard.clone()).expect("a conformant card");
823        assert_eq!(valid.version, VcardVersion::V4_0);
824
825        let inner = vcard.validate().unwrap().into_inner();
826        assert_eq!(inner.version, VcardVersion::V4_0);
827    }
828
829    /// The proof converts back into a byte tree, which only a build carrying
830    /// the syntax layer can do.
831    #[cfg(feature = "parser")]
832    #[test]
833    fn valid_proof_converts_into_a_byte_tree() {
834        use crate::tree::cst::VcardCst;
835
836        let vcard = card(
837            VcardVersion::V4_0,
838            vec![prop(
839                "FN",
840                vec![],
841                VcardValue::Text(VcardText(Cow::Borrowed("John"))),
842            )],
843        );
844
845        let valid = VcardValid::try_from(vcard).expect("a conformant card");
846        let cst = VcardCst::from(valid);
847
848        assert!(cst.to_string().contains("FN:John"));
849    }
850}