vcard-rs 0.4.0

vCard parser, validator, editor, merger and builder library
Documentation
//! # Property spec
//!
//! The per-property contract on the lens markers, and the runtime vtable that
//! bridges the open [`VcardPropKind`] back to those static impls.

use alloc::string::String;

use crate::{
    param::{COMMON_PARAMS, VcardParamKind},
    prop::{
        VcardPropKind, adr, agent, anniversary, bday, caladruri, caluri,
        cardinality::VcardPropCardinality, categories, class, client_pid_map, created, email,
        fburl, r#fn, gender, geo, gramgender, impp, jsprop, key, kind, label, lang, language, logo,
        mailer, member, n, name, nickname, note, org, photo, prodid, profile, pronouns, related,
        rev, role, socialprofile, sort_string, sound, source, tel, title, tz, uid, url, xml,
    },
    value::{VcardValue, VcardValueKind},
    version::VcardVersion,
};

/// The per-property contract: versions, multiplicity, value-types, parameters.
///
/// Implemented on the zero-sized lens markers, whose defaults cover the
/// uniform majority (one text value, every version), leaving only
/// [`KIND`](Self::KIND) required. [`value`](Self::value) resolves both axes.
pub trait VcardPropSpec {
    /// The property this spec describes.
    const KIND: VcardPropKind;

    /// The versions in which the property is defined (the existence axis).
    fn allowed_versions() -> &'static [VcardVersion] {
        &[VcardVersion::V2_1, VcardVersion::V3_0, VcardVersion::V4_0]
    }

    /// How many times the property may appear in a card, in the given version.
    /// Most properties are repeatable; the single-valued ones override this.
    fn cardinality(_version: VcardVersion) -> VcardPropCardinality {
        VcardPropCardinality::Any
    }

    /// The value-types the property may take, in default-first order, for the
    /// given version. Index 0 is the type used when no `VALUE` is declared.
    fn allowed_values(_version: VcardVersion) -> &'static [VcardValueKind] {
        &[VcardValueKind::Text]
    }

    /// The parameters the property may carry, in the given version.
    fn allowed_params(_version: VcardVersion) -> &'static [VcardParamKind] {
        COMMON_PARAMS
    }

    /// The value-type in force: the declared `VALUE` kind if any, else the
    /// version default ([`allowed_values`](Self::allowed_values)'s first), else
    /// [`Text`](VcardValueKind::Text). Liberal: a declared kind outside
    /// `allowed_values` is honoured here (membership is a validation concern).
    fn value(version: VcardVersion, declared: Option<VcardValueKind>) -> VcardValueKind {
        declared
            .or_else(|| Self::allowed_values(version).first().copied())
            .unwrap_or(VcardValueKind::Text)
    }

    /// What the value holds that the property's own definition forbids,
    /// `None` when it holds nothing of the sort.
    ///
    /// The other members of this spec describe a property's shape: which
    /// versions define it, how often it may appear, which value types and
    /// parameters it takes. This one is about the content of the value
    /// itself, and only the few properties whose RFC closes that content
    /// override it.
    ///
    /// Most do not, their grammars ending in `iana-token / x-name`, which
    /// is an open set and nothing to check against. `KIND`, `CLASS`,
    /// `GRAMGENDER` and the `TYPE` vocabularies are all open in exactly
    /// that way.
    fn invalid_value(_value: &VcardValue<'_>, _version: VcardVersion) -> Option<String> {
        None
    }
}

/// The spec of a property as function pointers, the runtime bridge from the
/// open [`VcardPropKind`] back to the static per-marker [`VcardPropSpec`]
/// impls. The decoder and the validator dispatch a prop kind through
/// [`prop_spec`] and then call these, instead of each owning a per-kind match.
pub(crate) struct VcardPropSpecFns {
    /// See [`VcardPropSpec::allowed_versions`].
    pub allowed_versions: fn() -> &'static [VcardVersion],
    /// See [`VcardPropSpec::cardinality`].
    pub cardinality: fn(VcardVersion) -> VcardPropCardinality,
    /// See [`VcardPropSpec::allowed_values`].
    pub allowed_values: fn(VcardVersion) -> &'static [VcardValueKind],
    /// See [`VcardPropSpec::allowed_params`].
    pub allowed_params: fn(VcardVersion) -> &'static [VcardParamKind],
    /// See [`VcardPropSpec::value`]. Read by the decoder and the jCard import,
    /// so it is dead in a build carrying neither.
    #[allow(dead_code)]
    pub value: fn(VcardVersion, Option<VcardValueKind>) -> VcardValueKind,
    /// See [`VcardPropSpec::invalid_value`].
    pub invalid_value: fn(&VcardValue<'_>, VcardVersion) -> Option<String>,
}

/// Collect the spec function pointers of a marker type.
fn spec_fns<L: VcardPropSpec>() -> VcardPropSpecFns {
    VcardPropSpecFns {
        allowed_versions: L::allowed_versions,
        cardinality: L::cardinality,
        allowed_values: L::allowed_values,
        allowed_params: L::allowed_params,
        value: L::value,
        invalid_value: L::invalid_value,
    }
}

/// Dispatch a property kind onto its marker spec.
pub(crate) fn prop_spec(prop: VcardPropKind) -> VcardPropSpecFns {
    use VcardPropKind::*;

    match prop {
        Adr => spec_fns::<adr::ADR>(),
        Agent => spec_fns::<agent::AGENT>(),
        Anniversary => spec_fns::<anniversary::ANNIVERSARY>(),
        Bday => spec_fns::<bday::BDAY>(),
        CalAdrUri => spec_fns::<caladruri::CALADRURI>(),
        CalUri => spec_fns::<caluri::CALURI>(),
        Categories => spec_fns::<categories::CATEGORIES>(),
        Class => spec_fns::<class::CLASS>(),
        ClientPidMap => spec_fns::<client_pid_map::CLIENTPIDMAP>(),
        Created => spec_fns::<created::CREATED>(),
        Email => spec_fns::<email::EMAIL>(),
        FbUrl => spec_fns::<fburl::FBURL>(),
        Fn => spec_fns::<r#fn::FN>(),
        Gender => spec_fns::<gender::GENDER>(),
        Geo => spec_fns::<geo::GEO>(),
        GramGender => spec_fns::<gramgender::GRAMGENDER>(),
        Impp => spec_fns::<impp::IMPP>(),
        JsProp => spec_fns::<jsprop::JSPROP>(),
        Key => spec_fns::<key::KEY>(),
        Kind => spec_fns::<kind::KIND>(),
        Label => spec_fns::<label::LABEL>(),
        Lang => spec_fns::<lang::LANG>(),
        Language => spec_fns::<language::LANGUAGE>(),
        Logo => spec_fns::<logo::LOGO>(),
        Mailer => spec_fns::<mailer::MAILER>(),
        Member => spec_fns::<member::MEMBER>(),
        N => spec_fns::<n::N>(),
        Name => spec_fns::<name::NAME>(),
        Nickname => spec_fns::<nickname::NICKNAME>(),
        Note => spec_fns::<note::NOTE>(),
        Org => spec_fns::<org::ORG>(),
        Photo => spec_fns::<photo::PHOTO>(),
        ProdId => spec_fns::<prodid::PRODID>(),
        Profile => spec_fns::<profile::PROFILE>(),
        Pronouns => spec_fns::<pronouns::PRONOUNS>(),
        Related => spec_fns::<related::RELATED>(),
        Rev => spec_fns::<rev::REV>(),
        Role => spec_fns::<role::ROLE>(),
        SocialProfile => spec_fns::<socialprofile::SOCIALPROFILE>(),
        SortString => spec_fns::<sort_string::SORT_STRING>(),
        Sound => spec_fns::<sound::SOUND>(),
        Source => spec_fns::<source::SOURCE>(),
        Tel => spec_fns::<tel::TEL>(),
        Title => spec_fns::<title::TITLE>(),
        Tz => spec_fns::<tz::TZ>(),
        Uid => spec_fns::<uid::UID>(),
        Url => spec_fns::<url::URL>(),
        Xml => spec_fns::<xml::XML>(),
    }
}

#[cfg(test)]
mod tests {
    use crate::{
        prop::VcardPropKind, prop::spec::prop_spec, value::VcardValueKind, version::VcardVersion,
    };

    #[test]
    fn every_property_spec_answers_for_every_version() {
        for kind in VcardPropKind::ALL {
            let spec = prop_spec(kind);

            for version in [VcardVersion::V2_1, VcardVersion::V3_0, VcardVersion::V4_0] {
                let _ = (spec.allowed_versions)();
                let _ = (spec.cardinality)(version);
                let _ = (spec.allowed_values)(version);
                let _ = (spec.allowed_params)(version);
                let _ = (spec.value)(version, None);
                let _ = (spec.value)(version, Some(VcardValueKind::Uri));
            }
        }
    }
}