use core::{error, fmt, ops, str};
use alloc::{
borrow::Cow,
string::{String, ToString},
vec::Vec,
};
use crate::{param::VcardParam, value::VcardValue};
#[derive(Debug)]
pub struct ParseVcardPropKindError(
String,
);
impl fmt::Display for ParseVcardPropKindError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Cannot parse vCard property `{}`", self.0)
}
}
impl error::Error for ParseVcardPropKindError {}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VcardProp<'a> {
pub name: VcardPropName<'a>,
pub params: Vec<VcardParam<'a>>,
pub value: VcardValue<'a>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum VcardPropName<'a> {
Kind(VcardPropKind),
Unknown(Cow<'a, str>),
}
impl ops::Deref for VcardPropName<'_> {
type Target = str;
fn deref(&self) -> &Self::Target {
match self {
Self::Kind(kind) => kind,
Self::Unknown(name) => name,
}
}
}
impl From<VcardPropKind> for VcardPropName<'_> {
fn from(kind: VcardPropKind) -> Self {
Self::Kind(kind)
}
}
impl From<&VcardPropKind> for VcardPropName<'_> {
fn from(kind: &VcardPropKind) -> Self {
Self::Kind(*kind)
}
}
impl<'a> From<Cow<'a, str>> for VcardPropName<'a> {
fn from(kind: Cow<'a, str>) -> Self {
match kind.parse().ok() {
Some(kind) => Self::Kind(kind),
None => Self::Unknown(kind),
}
}
}
impl<'a> From<&'a str> for VcardPropName<'a> {
fn from(name: &'a str) -> Self {
Cow::Borrowed(name).into()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum VcardPropKind {
Adr,
Agent,
Anniversary,
Bday,
CalAdrUri,
CalUri,
Categories,
Class,
ClientPidMap,
Created,
Email,
FbUrl,
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,
SortString,
Sound,
Source,
Tel,
Title,
Tz,
Uid,
Url,
Xml,
}
impl VcardPropKind {
pub const ALL: [Self; 48] = [
Self::Adr,
Self::Agent,
Self::Anniversary,
Self::Bday,
Self::CalAdrUri,
Self::CalUri,
Self::Categories,
Self::Class,
Self::ClientPidMap,
Self::Created,
Self::Email,
Self::FbUrl,
Self::Fn,
Self::Gender,
Self::Geo,
Self::GramGender,
Self::Impp,
Self::JsProp,
Self::Key,
Self::Kind,
Self::Label,
Self::Lang,
Self::Language,
Self::Logo,
Self::Mailer,
Self::Member,
Self::N,
Self::Name,
Self::Nickname,
Self::Note,
Self::Org,
Self::Photo,
Self::ProdId,
Self::Profile,
Self::Pronouns,
Self::Related,
Self::Rev,
Self::Role,
Self::SocialProfile,
Self::SortString,
Self::Sound,
Self::Source,
Self::Tel,
Self::Title,
Self::Tz,
Self::Uid,
Self::Url,
Self::Xml,
];
}
impl str::FromStr for VcardPropKind {
type Err = ParseVcardPropKindError;
fn from_str(kind: &str) -> Result<Self, Self::Err> {
match kind {
kind if kind.eq_ignore_ascii_case("ADR") => Ok(Self::Adr),
kind if kind.eq_ignore_ascii_case("AGENT") => Ok(Self::Agent),
kind if kind.eq_ignore_ascii_case("ANNIVERSARY") => Ok(Self::Anniversary),
kind if kind.eq_ignore_ascii_case("BDAY") => Ok(Self::Bday),
kind if kind.eq_ignore_ascii_case("CALADRURI") => Ok(Self::CalAdrUri),
kind if kind.eq_ignore_ascii_case("CALURI") => Ok(Self::CalUri),
kind if kind.eq_ignore_ascii_case("CATEGORIES") => Ok(Self::Categories),
kind if kind.eq_ignore_ascii_case("CLASS") => Ok(Self::Class),
kind if kind.eq_ignore_ascii_case("CLIENTPIDMAP") => Ok(Self::ClientPidMap),
kind if kind.eq_ignore_ascii_case("CREATED") => Ok(Self::Created),
kind if kind.eq_ignore_ascii_case("EMAIL") => Ok(Self::Email),
kind if kind.eq_ignore_ascii_case("FBURL") => Ok(Self::FbUrl),
kind if kind.eq_ignore_ascii_case("FN") => Ok(Self::Fn),
kind if kind.eq_ignore_ascii_case("GENDER") => Ok(Self::Gender),
kind if kind.eq_ignore_ascii_case("GEO") => Ok(Self::Geo),
kind if kind.eq_ignore_ascii_case("GRAMGENDER") => Ok(Self::GramGender),
kind if kind.eq_ignore_ascii_case("IMPP") => Ok(Self::Impp),
kind if kind.eq_ignore_ascii_case("JSPROP") => Ok(Self::JsProp),
kind if kind.eq_ignore_ascii_case("KEY") => Ok(Self::Key),
kind if kind.eq_ignore_ascii_case("KIND") => Ok(Self::Kind),
kind if kind.eq_ignore_ascii_case("LABEL") => Ok(Self::Label),
kind if kind.eq_ignore_ascii_case("LANG") => Ok(Self::Lang),
kind if kind.eq_ignore_ascii_case("LANGUAGE") => Ok(Self::Language),
kind if kind.eq_ignore_ascii_case("LOGO") => Ok(Self::Logo),
kind if kind.eq_ignore_ascii_case("MAILER") => Ok(Self::Mailer),
kind if kind.eq_ignore_ascii_case("MEMBER") => Ok(Self::Member),
kind if kind.eq_ignore_ascii_case("N") => Ok(Self::N),
kind if kind.eq_ignore_ascii_case("NAME") => Ok(Self::Name),
kind if kind.eq_ignore_ascii_case("NICKNAME") => Ok(Self::Nickname),
kind if kind.eq_ignore_ascii_case("NOTE") => Ok(Self::Note),
kind if kind.eq_ignore_ascii_case("ORG") => Ok(Self::Org),
kind if kind.eq_ignore_ascii_case("PHOTO") => Ok(Self::Photo),
kind if kind.eq_ignore_ascii_case("PRODID") => Ok(Self::ProdId),
kind if kind.eq_ignore_ascii_case("PROFILE") => Ok(Self::Profile),
kind if kind.eq_ignore_ascii_case("PRONOUNS") => Ok(Self::Pronouns),
kind if kind.eq_ignore_ascii_case("RELATED") => Ok(Self::Related),
kind if kind.eq_ignore_ascii_case("REV") => Ok(Self::Rev),
kind if kind.eq_ignore_ascii_case("ROLE") => Ok(Self::Role),
kind if kind.eq_ignore_ascii_case("SOCIALPROFILE") => Ok(Self::SocialProfile),
kind if kind.eq_ignore_ascii_case("SORT-STRING") => Ok(Self::SortString),
kind if kind.eq_ignore_ascii_case("SOUND") => Ok(Self::Sound),
kind if kind.eq_ignore_ascii_case("SOURCE") => Ok(Self::Source),
kind if kind.eq_ignore_ascii_case("TEL") => Ok(Self::Tel),
kind if kind.eq_ignore_ascii_case("TITLE") => Ok(Self::Title),
kind if kind.eq_ignore_ascii_case("TZ") => Ok(Self::Tz),
kind if kind.eq_ignore_ascii_case("UID") => Ok(Self::Uid),
kind if kind.eq_ignore_ascii_case("URL") => Ok(Self::Url),
kind if kind.eq_ignore_ascii_case("XML") => Ok(Self::Xml),
_ => Err(ParseVcardPropKindError(kind.to_string())),
}
}
}
impl ops::Deref for VcardPropKind {
type Target = str;
fn deref(&self) -> &Self::Target {
match self {
Self::Adr => "ADR",
Self::Agent => "AGENT",
Self::Anniversary => "ANNIVERSARY",
Self::Bday => "BDAY",
Self::CalAdrUri => "CALADRURI",
Self::CalUri => "CALURI",
Self::Categories => "CATEGORIES",
Self::Class => "CLASS",
Self::ClientPidMap => "CLIENTPIDMAP",
Self::Created => "CREATED",
Self::Email => "EMAIL",
Self::FbUrl => "FBURL",
Self::Fn => "FN",
Self::Gender => "GENDER",
Self::Geo => "GEO",
Self::GramGender => "GRAMGENDER",
Self::Impp => "IMPP",
Self::JsProp => "JSPROP",
Self::Key => "KEY",
Self::Kind => "KIND",
Self::Label => "LABEL",
Self::Lang => "LANG",
Self::Language => "LANGUAGE",
Self::Logo => "LOGO",
Self::Mailer => "MAILER",
Self::Member => "MEMBER",
Self::N => "N",
Self::Name => "NAME",
Self::Nickname => "NICKNAME",
Self::Note => "NOTE",
Self::Org => "ORG",
Self::Photo => "PHOTO",
Self::ProdId => "PRODID",
Self::Profile => "PROFILE",
Self::Pronouns => "PRONOUNS",
Self::Related => "RELATED",
Self::Rev => "REV",
Self::Role => "ROLE",
Self::SocialProfile => "SOCIALPROFILE",
Self::SortString => "SORT-STRING",
Self::Sound => "SOUND",
Self::Source => "SOURCE",
Self::Tel => "TEL",
Self::Title => "TITLE",
Self::Tz => "TZ",
Self::Uid => "UID",
Self::Url => "URL",
Self::Xml => "XML",
}
}
}
#[cfg(test)]
mod tests {
use core::str::FromStr;
use alloc::{borrow::Cow, vec};
use crate::{
param::VcardParam,
prop::{VcardProp, VcardPropKind, VcardPropName},
value::{VcardValue, text::VcardText},
};
#[test]
fn names_the_property_and_wraps_the_value() {
let prop = VcardProp {
name: VcardPropKind::Title.into(),
params: [].into(),
value: VcardValue::Text(VcardText(Cow::Borrowed("Developer"))),
};
assert_eq!(prop.name, VcardPropName::Kind(VcardPropKind::Title));
assert_eq!(&*prop.name, "TITLE");
assert_eq!(
prop.value,
VcardValue::Text(VcardText(Cow::Borrowed("Developer"))),
);
assert!(prop.params.is_empty());
}
#[test]
fn carries_the_given_parameters() {
let prop = VcardProp {
name: VcardPropKind::Fn.into(),
params: [VcardParam::Pref(Cow::Borrowed("1"))].into(),
value: VcardValue::Text(VcardText(Cow::Borrowed("John"))),
};
assert_eq!(&*prop.name, "FN");
assert_eq!(prop.params, vec![VcardParam::Pref(Cow::Borrowed("1"))]);
}
#[test]
fn round_trips_every_kind_through_its_wire_name() {
for kind in [
VcardPropKind::Fn,
VcardPropKind::ClientPidMap,
VcardPropKind::SortString,
VcardPropKind::CalAdrUri,
] {
assert_eq!(VcardPropKind::from_str(&kind).ok(), Some(kind));
}
assert_eq!(VcardPropKind::from_str("fn").ok(), Some(VcardPropKind::Fn));
assert!(VcardPropKind::from_str("X-CUSTOM").is_err());
}
}