use std::{
fmt::{self, Display, Formatter, Write as _},
fs, io,
path::Path,
};
use crate::{
error::ValidationError,
fold::FoldingWriter,
property::{
Address, Anniversary, Birthday, CalendarAddressUri, CalendarUri, Categories, ClientPidMap,
Created, Email, ExtensionProperty, Fburl, FormattedName, Gender, Geo, GramGender, Impp,
Key, Kind, Lang, Language, Logo, Member, Name, Nickname, Note, Org, Photo, ProdId,
Pronouns, Related, Rev, Role, SocialProfile, Sound, Source, Tel, TimeZone, Title, Uid, Url,
Xml, write_property,
},
values::KindValue,
};
macro_rules! for_each_property {
($callback:ident) => {
$callback! {
(sources, "SOURCE", many),
(kind, "KIND", one),
(xmls, "XML", many),
(formatted_names, "FN", many),
(names, "N", many),
(nicknames, "NICKNAME", many),
(photos, "PHOTO", many),
(birthday, "BDAY", one),
(anniversary, "ANNIVERSARY", one),
(gender, "GENDER", one),
(addresses, "ADR", many),
(telephones, "TEL", many),
(emails, "EMAIL", many),
(impps, "IMPP", many),
(langs, "LANG", many),
(time_zones, "TZ", many),
(geos, "GEO", many),
(titles, "TITLE", many),
(roles, "ROLE", many),
(logos, "LOGO", many),
(organizations, "ORG", many),
(members, "MEMBER", many),
(relations, "RELATED", many),
(categories, "CATEGORIES", many),
(notes, "NOTE", many),
(product_id, "PRODID", one),
(revision, "REV", one),
(sounds, "SOUND", many),
(uid, "UID", one),
(client_pid_maps, "CLIENTPIDMAP", many),
(urls, "URL", many),
(keys, "KEY", many),
(fburls, "FBURL", many),
(calendar_address_uris, "CALADRURI", many),
(calendar_uris, "CALURI", many),
(created, "CREATED", one),
(gram_genders, "GRAMGENDER", many),
(language, "LANGUAGE", one),
(pronouns, "PRONOUNS", many),
(social_profiles, "SOCIALPROFILE", many),
}
};
}
pub(crate) use for_each_property;
#[derive(Debug, Clone, PartialEq, Default)]
pub struct VCard {
pub sources: Vec<Source>,
pub kind: Option<Kind>,
pub xmls: Vec<Xml>,
pub formatted_names: Vec<FormattedName>,
pub names: Vec<Name>,
pub nicknames: Vec<Nickname>,
pub photos: Vec<Photo>,
pub birthday: Option<Birthday>,
pub anniversary: Option<Anniversary>,
pub gender: Option<Gender>,
pub addresses: Vec<Address>,
pub telephones: Vec<Tel>,
pub emails: Vec<Email>,
pub impps: Vec<Impp>,
pub langs: Vec<Lang>,
pub time_zones: Vec<TimeZone>,
pub geos: Vec<Geo>,
pub titles: Vec<Title>,
pub roles: Vec<Role>,
pub logos: Vec<Logo>,
pub organizations: Vec<Org>,
pub members: Vec<Member>,
pub relations: Vec<Related>,
pub categories: Vec<Categories>,
pub notes: Vec<Note>,
pub product_id: Option<ProdId>,
pub revision: Option<Rev>,
pub sounds: Vec<Sound>,
pub uid: Option<Uid>,
pub client_pid_maps: Vec<ClientPidMap>,
pub urls: Vec<Url>,
pub keys: Vec<Key>,
pub fburls: Vec<Fburl>,
pub calendar_address_uris: Vec<CalendarAddressUri>,
pub calendar_uris: Vec<CalendarUri>,
pub created: Option<Created>,
pub gram_genders: Vec<GramGender>,
pub language: Option<Language>,
pub pronouns: Vec<Pronouns>,
pub social_profiles: Vec<SocialProfile>,
pub extensions: Vec<ExtensionProperty>,
}
impl VCard {
#[inline]
pub fn new<S: Into<String>>(formatted_name: S) -> Self {
Self {
formatted_names: vec![FormattedName::new(formatted_name.into())],
..Self::default()
}
}
pub fn validate(&self) -> Result<(), ValidationError> {
if self.formatted_names.is_empty() {
return Err(ValidationError::MissingFormattedName);
}
if !self.members.is_empty()
&& !self.kind.as_ref().is_some_and(|kind| kind.value == KindValue::Group)
{
return Err(ValidationError::MemberWithoutGroupKind);
}
Ok(())
}
#[inline]
pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), io::Error> {
fs::write(path, self.to_string())
}
}
macro_rules! write_field {
(many, $vcard:expr, $field:ident, $name:literal, $w:expr) => {
for property in &$vcard.$field {
write_property($w, $name, property)?;
}
};
(one, $vcard:expr, $field:ident, $name:literal, $w:expr) => {
if let Some(property) = &$vcard.$field {
write_property($w, $name, property)?;
}
};
}
impl Display for VCard {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let mut w = FoldingWriter::new(f);
w.write_str("BEGIN:VCARD")?;
w.end_line()?;
w.write_str("VERSION:4.0")?;
w.end_line()?;
macro_rules! write_fields {
($(($field:ident, $name:literal, $card:tt)),* $(,)?) => {
$(write_field!($card, self, $field, $name, &mut w);)*
};
}
for_each_property!(write_fields);
for extension in &self.extensions {
extension.write(&mut w)?;
}
w.write_str("END:VCARD")?;
w.end_line()
}
}