Skip to main content

vcard/
value.rs

1//! # Property values
2//!
3//! The decoded value of a property, one variant per RFC 6350 value kind.
4//!
5//! [`VcardValue`] is the semantic counterpart of a content line's raw value
6//! (the syntactic
7//! [`VcardValueNode`](crate::tree::value::node::VcardValueNode)). Most
8//! properties share a small set of kinds: a single text, a text list, a URI, a
9//! date/time, a timestamp, a UTC offset, a language tag. The genuinely
10//! structured ones get a bespoke type in a submodule here ([`n::VcardN`],
11//! [`adr::VcardAdr`], [`gender::VcardGender`], [`org::VcardOrg`],
12//! [`client_pid_map::VcardClientPidMap`]). Anything the model does not decode
13//! falls back to [`Unknown`](VcardValue::Unknown), which keeps the raw
14//! components so it round-trips.
15//!
16//! These types carry no wire name and no escaping: the name lives on
17//! [`VcardProp::name`](crate::prop::VcardProp::name), the escaping and framing
18//! on the syntax side, so the decoded model stays free of [`crate::tree`].
19
20pub mod adr;
21pub mod binary;
22pub mod client_pid_map;
23pub mod datetime;
24pub mod gender;
25pub mod geo;
26pub mod language;
27pub mod n;
28pub mod org;
29pub mod text;
30pub mod uri;
31pub mod utc_offset;
32
33use core::{error, fmt, ops, str};
34
35use alloc::{
36    borrow::Cow,
37    string::{String, ToString},
38    vec::Vec,
39};
40
41use crate::value::{
42    adr::VcardAdr,
43    binary::VcardBinary,
44    client_pid_map::VcardClientPidMap,
45    datetime::{VcardDateAndOrTime, VcardTimestamp},
46    gender::VcardGender,
47    geo::VcardGeo,
48    language::VcardLanguageTag,
49    n::VcardN,
50    org::VcardOrg,
51    text::{VcardText, VcardTextList},
52    uri::VcardUri,
53    utc_offset::VcardUtcOffset,
54};
55
56/// Parse vCard value kind error.
57#[derive(Debug)]
58pub struct VcardValueKindParseError(
59    /// The vCard value type that cannot be parsed.
60    String,
61);
62
63impl fmt::Display for VcardValueKindParseError {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        write!(f, "Cannot parse vCard value type `{}`", self.0)
66    }
67}
68
69impl error::Error for VcardValueKindParseError {}
70
71/// The closed RFC 6350 value-type vocabulary, one fieldless variant per value
72/// kind. It is the discriminant of [`VcardValue`] (which also has an `Unknown`
73/// arm outside this closed set) and the currency of the prop spec's
74/// allowed-values sets.
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76pub enum VcardValueKind {
77    /// The structured `ADR` value (RFC 6350 6.3.1).
78    Adr,
79    /// An inline-base64 or URI-reference binary value (vCard 2.1 / 3.0).
80    Binary,
81    /// The structured `CLIENTPIDMAP` value (RFC 6350 6.7.7).
82    ClientPidMap,
83    /// A date-and-or-time value (RFC 6350 4.3.4).
84    DateAndOrTime,
85    /// The structured `GENDER` value (RFC 6350 6.2.7).
86    Gender,
87    /// A latitude/longitude pair (vCard 2.1 / 3.0 `GEO`).
88    Geo,
89    /// A language tag (RFC 6350 4.8).
90    LanguageTag,
91    /// The structured `N` value (RFC 6350 6.2.2).
92    N,
93    /// The structured `ORG` value (RFC 6350 6.6.4).
94    Org,
95    /// A single text value (RFC 6350 4.1).
96    Text,
97    /// A comma-separated text list (RFC 6350 4.1).
98    TextList,
99    /// A timestamp (RFC 6350 4.3.5).
100    Timestamp,
101    /// A URI (RFC 6350 4.2).
102    Uri,
103    /// A UTC offset (RFC 6350 4.7).
104    UtcOffset,
105}
106
107impl str::FromStr for VcardValueKind {
108    type Err = VcardValueKindParseError;
109
110    /// The value kind named by a `VALUE` parameter (case-insensitive). Liberal:
111    /// it maps every wire spelling (and a few aliases) onto a model kind,
112    /// leaving membership checks to a later validation tier.
113    fn from_str(kind: &str) -> Result<Self, Self::Err> {
114        match kind {
115            kind if kind.eq_ignore_ascii_case("ADR") => Ok(Self::Adr),
116            kind if kind.eq_ignore_ascii_case("B") => Ok(Self::Binary),
117            kind if kind.eq_ignore_ascii_case("BINARY") => Ok(Self::Binary),
118            kind if kind.eq_ignore_ascii_case("CLIENTPIDMAP") => Ok(Self::ClientPidMap),
119            kind if kind.eq_ignore_ascii_case("DATE") => Ok(Self::DateAndOrTime),
120            kind if kind.eq_ignore_ascii_case("DATE-AND-OR-TIME") => Ok(Self::DateAndOrTime),
121            kind if kind.eq_ignore_ascii_case("DATE-TIME") => Ok(Self::DateAndOrTime),
122            kind if kind.eq_ignore_ascii_case("GENDER") => Ok(Self::Gender),
123            kind if kind.eq_ignore_ascii_case("GEO") => Ok(Self::Geo),
124            kind if kind.eq_ignore_ascii_case("LANGUAGE-TAG") => Ok(Self::LanguageTag),
125            kind if kind.eq_ignore_ascii_case("N") => Ok(Self::N),
126            kind if kind.eq_ignore_ascii_case("ORG") => Ok(Self::Org),
127            kind if kind.eq_ignore_ascii_case("TEXT") => Ok(Self::Text),
128            kind if kind.eq_ignore_ascii_case("TEXT-LIST") => Ok(Self::TextList),
129            kind if kind.eq_ignore_ascii_case("TIME") => Ok(Self::DateAndOrTime),
130            kind if kind.eq_ignore_ascii_case("TIMESTAMP") => Ok(Self::Timestamp),
131            kind if kind.eq_ignore_ascii_case("URI") => Ok(Self::Uri),
132            kind if kind.eq_ignore_ascii_case("URL") => Ok(Self::Uri),
133            kind if kind.eq_ignore_ascii_case("UTC-OFFSET") => Ok(Self::UtcOffset),
134            _ => Err(VcardValueKindParseError(kind.to_string())),
135        }
136    }
137}
138
139impl ops::Deref for VcardValueKind {
140    type Target = str;
141
142    fn deref(&self) -> &Self::Target {
143        match self {
144            Self::Adr => "ADR",
145            Self::Binary => "BINARY",
146            Self::ClientPidMap => "CLIENTPIDMAP",
147            Self::DateAndOrTime => "DATE-AND-OR-TIME",
148            Self::Gender => "GENDER",
149            Self::Geo => "GEO",
150            Self::LanguageTag => "LANGUAGE-TAG",
151            Self::N => "N",
152            Self::Org => "ORG",
153            Self::Text => "TEXT",
154            Self::TextList => "TEXT-LIST",
155            Self::Timestamp => "TIMESTAMP",
156            Self::Uri => "URI",
157            Self::UtcOffset => "UTC-OFFSET",
158        }
159    }
160}
161
162/// A decoded property value: one known kind, or `Unknown` (raw) for anything
163/// the model does not decode.
164// NOTE: the 18-component VcardAdr dominates the enum size; values are
165// decoded on demand, not stored in bulk, so plain variants beat boxing.
166#[allow(clippy::large_enum_variant)]
167#[derive(Clone, Debug, PartialEq, Eq)]
168pub enum VcardValue<'a> {
169    /// The structured `ADR` value.
170    Adr(VcardAdr<'a>),
171    /// A binary value (2.1 / 3.0 `PHOTO`, `LOGO`, `SOUND`, `KEY`): a URI
172    /// reference or inline base64.
173    Binary(VcardBinary<'a>),
174    /// The structured `CLIENTPIDMAP` value.
175    ClientPidMap(VcardClientPidMap<'a>),
176    /// A date-and-or-time (`BDAY`, `ANNIVERSARY`).
177    DateAndOrTime(VcardDateAndOrTime<'a>),
178    /// The structured `GENDER` value.
179    Gender(VcardGender<'a>),
180    /// A `GEO` latitude/longitude pair (2.1 / 3.0; 4.0 uses a URI).
181    Geo(VcardGeo<'a>),
182    /// A language tag (`LANG`).
183    LanguageTag(VcardLanguageTag<'a>),
184    /// The structured `N` value.
185    N(VcardN<'a>),
186    /// The structured `ORG` value.
187    Org(VcardOrg<'a>),
188    /// A single text value (`FN`, `TITLE`, `NOTE`, ...).
189    Text(VcardText<'a>),
190    /// A comma-separated text list (`NICKNAME`, `CATEGORIES`).
191    TextList(VcardTextList<'a>),
192    /// A timestamp (`REV`).
193    Timestamp(VcardTimestamp<'a>),
194    /// A URI (`PHOTO`, `URL`, `KEY`, ...).
195    Uri(VcardUri<'a>),
196    /// A UTC offset (one form of `TZ`).
197    UtcOffset(VcardUtcOffset<'a>),
198    /// Any value the model does not decode, kept as its raw components so it
199    /// round-trips.
200    Unknown(VcardValueUnknown<'a>),
201}
202
203impl VcardValue<'_> {
204    /// The closed [`VcardValueKind`] of this value, or `None` for
205    /// [`Unknown`](VcardValue::Unknown) (which is outside the vocabulary).
206    pub fn kind(&self) -> Option<VcardValueKind> {
207        match self {
208            Self::Adr(_) => Some(VcardValueKind::Adr),
209            Self::Binary(_) => Some(VcardValueKind::Binary),
210            Self::ClientPidMap(_) => Some(VcardValueKind::ClientPidMap),
211            Self::DateAndOrTime(_) => Some(VcardValueKind::DateAndOrTime),
212            Self::Gender(_) => Some(VcardValueKind::Gender),
213            Self::Geo(_) => Some(VcardValueKind::Geo),
214            Self::LanguageTag(_) => Some(VcardValueKind::LanguageTag),
215            Self::N(_) => Some(VcardValueKind::N),
216            Self::Org(_) => Some(VcardValueKind::Org),
217            Self::Text(_) => Some(VcardValueKind::Text),
218            Self::TextList(_) => Some(VcardValueKind::TextList),
219            Self::Timestamp(_) => Some(VcardValueKind::Timestamp),
220            Self::Uri(_) => Some(VcardValueKind::Uri),
221            Self::UtcOffset(_) => Some(VcardValueKind::UtcOffset),
222            Self::Unknown(_) => None,
223        }
224    }
225
226    /// An empty value of the given kind: the inverse of [`kind`](Self::kind),
227    /// so `empty(k).kind() == Some(k)`. Every component is blank (an empty
228    /// string, list, or structured value); a binary is an empty URI
229    /// reference. Used to mint a placeholder for a required property that is
230    /// otherwise absent (see [`VcardCst::fill_required`]).
231    ///
232    /// [`VcardCst::fill_required`]: crate::tree::cst::VcardCst::fill_required
233    pub fn empty(kind: VcardValueKind) -> VcardValue<'static> {
234        match kind {
235            VcardValueKind::Adr => VcardValue::Adr(VcardAdr::default()),
236            VcardValueKind::Binary => VcardValue::Binary(VcardBinary::Uri(Cow::Borrowed(""))),
237            VcardValueKind::ClientPidMap => VcardValue::ClientPidMap(VcardClientPidMap::default()),
238            VcardValueKind::DateAndOrTime => {
239                VcardValue::DateAndOrTime(VcardDateAndOrTime::default())
240            }
241            VcardValueKind::Gender => VcardValue::Gender(VcardGender::default()),
242            VcardValueKind::Geo => VcardValue::Geo(VcardGeo::default()),
243            VcardValueKind::LanguageTag => VcardValue::LanguageTag(VcardLanguageTag::default()),
244            VcardValueKind::N => VcardValue::N(VcardN::default()),
245            VcardValueKind::Org => VcardValue::Org(VcardOrg::default()),
246            VcardValueKind::Text => VcardValue::Text(VcardText::default()),
247            VcardValueKind::TextList => VcardValue::TextList(VcardTextList::default()),
248            VcardValueKind::Timestamp => VcardValue::Timestamp(VcardTimestamp::default()),
249            VcardValueKind::Uri => VcardValue::Uri(VcardUri::default()),
250            VcardValueKind::UtcOffset => VcardValue::UtcOffset(VcardUtcOffset::default()),
251        }
252    }
253}
254
255/// An undecoded property value: its unescaped components, in source order. The
256/// property name lives on [`VcardProp::name`](crate::prop::VcardProp::name).
257#[derive(Clone, Debug, Default, PartialEq, Eq)]
258pub struct VcardValueUnknown<'a> {
259    /// The value, as components of values.
260    pub components: Vec<Vec<Cow<'a, str>>>,
261}
262
263#[cfg(test)]
264mod tests {
265    use core::str::FromStr;
266
267    use crate::value::{VcardValue, VcardValueKind, VcardValueUnknown, text::VcardText};
268
269    #[test]
270    fn empty_is_the_inverse_of_kind() {
271        use crate::value::VcardValueKind::*;
272
273        for kind in [
274            Adr,
275            Binary,
276            ClientPidMap,
277            DateAndOrTime,
278            Gender,
279            Geo,
280            LanguageTag,
281            N,
282            Org,
283            Text,
284            TextList,
285            Timestamp,
286            Uri,
287            UtcOffset,
288        ] {
289            assert_eq!(VcardValue::empty(kind).kind(), Some(kind));
290        }
291    }
292
293    #[test]
294    fn reports_the_kind_of_a_value_and_none_for_unknown() {
295        assert_eq!(
296            VcardValue::Text(VcardText::default()).kind(),
297            Some(VcardValueKind::Text),
298        );
299        assert_eq!(
300            VcardValue::Unknown(VcardValueUnknown::default()).kind(),
301            None,
302        );
303    }
304
305    #[test]
306    fn maps_value_param_strings_liberally_and_case_insensitively() {
307        assert_eq!("URI".parse().ok(), Some(VcardValueKind::Uri));
308        assert_eq!("date".parse().ok(), Some(VcardValueKind::DateAndOrTime));
309        assert!(VcardValueKind::from_str("bogus").is_err());
310    }
311}