diff options
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | src/error.rs | 6 | ||||
-rw-r--r-- | src/lib.rs | 2 | ||||
-rw-r--r-- | src/param.rs | 16 | ||||
-rw-r--r-- | src/vcard.rs | 376 |
5 files changed, 401 insertions, 1 deletions
@@ -13,5 +13,5 @@ authors = ["Markus Unterwaditzer <markus@unterwaditzer.net>"] license = "MIT" [dependencies] -error-chain = "0.10" +error-chain = "0.11" diff --git a/src/error.rs b/src/error.rs index d772e5e..cf34910 100644 --- a/src/error.rs +++ b/src/error.rs @@ -10,6 +10,12 @@ error_chain! { description("Parser error") display("{}", desc) } + + NotAVCard { + description("Input is not a valid VCard") + display("Passed content string is not a VCard") + } + } @@ -3,10 +3,12 @@ #[macro_use] extern crate error_chain; +#[macro_use] pub mod param; pub mod component; pub mod error; mod parser; pub mod property; +pub mod vcard; pub use component::Component; pub use component::parse_component; diff --git a/src/param.rs b/src/param.rs new file mode 100644 index 0000000..daccc45 --- /dev/null +++ b/src/param.rs @@ -0,0 +1,16 @@ +use std::collections::BTreeMap; + +pub type Parameters = BTreeMap<String, String>; + +#[macro_export] +macro_rules! parameters( + { $($key:expr => $value:expr),* } => { + #[allow(unused_mut)] + { + let mut m : ::std::collections::BTreeMap<String, String> = + ::std::collections::BTreeMap::new(); + $( m.insert($key.into(), $value.into()); )* + m + } + }; +); diff --git a/src/vcard.rs b/src/vcard.rs new file mode 100644 index 0000000..b4a3e87 --- /dev/null +++ b/src/vcard.rs @@ -0,0 +1,376 @@ +use std::ops::Deref; + +use component::Component; +use component::parse_component; +use property::Property; + +use std::result::Result as RResult; +use error::*; +use param::Parameters; + +pub struct Vcard(Component); + +macro_rules! make_getter_function_for_optional { + ($fnname:ident, $name:expr, $mapper:ty) => { + pub fn $fnname(&self) -> Option<$mapper> { + self.0.get_only($name).cloned().map(From::from) + } + } +} + +macro_rules! make_getter_function_for_values { + ($fnname:ident, $name:expr, $mapper:ty) => { + pub fn $fnname(&self) -> Vec<$mapper> { + self.0 + .get_all($name) + .iter() + .map(Clone::clone) + .map(From::from) + .collect() + } + } +} + +macro_rules! make_builder_fn { + ( + fn $fnname:ident building $property_name:tt with_params, + $mapfn:expr => $( $arg_name:ident : $arg_type:ty ),* + ) => { + pub fn $fnname(mut self, params: Parameters, $( $arg_name : $arg_type ),*) -> Self { + let raw_value = vec![ $( $arg_name ),* ] + .into_iter() + .map($mapfn) + .collect::<Vec<_>>() + .join(";"); + + let mut prop = Property::new(String::from($property_name), raw_value); + prop.params = params; + self.0.props.entry(String::from($property_name)).or_insert(vec![]).push(prop); + self + } + }; + + ( + fn $fnname:ident building $property_name:tt, + $mapfn:expr => $( $arg_name:ident : $arg_type:ty ),* + ) => { + pub fn $fnname(mut self, $( $arg_name : $arg_type ),*) -> Self { + let raw_value = vec![ $( $arg_name ),* ] + .into_iter() + .map($mapfn) + .collect::<Vec<_>>() + .join(";"); + + let prop = Property::new(String::from($property_name), raw_value); + self.0.props.entry(String::from($property_name)).or_insert(vec![]).push(prop); + self + } + } +} + + +/// The Vcard object. +/// +/// This type simply holds data and offers functions to access this data. It does not compute +/// anything. +impl Vcard { + + /// Parse a string to a Vcard object + /// + /// Returns an error if the parsed text is not a Vcard (that means that an error is returned + /// also if this is a valid icalendar!) + /// + pub fn build(s: &str) -> Result<Vcard> { + parse_component(s) + .and_then(|c| { + Self::from_component(c) + .map_err(|_| { + let kind = VObjectErrorKind::NotAVCard; + VObjectError::from_kind(kind) + }) + }) + } + + /// Wrap a Component into a Vcard object, or don't do it if the Component is not a Vcard. + pub fn from_component(c: Component)-> RResult<Vcard, Component> { + if c.name == "VCARD" { + Ok(Vcard(c)) + } else { + Err(c) + } + } + + make_getter_function_for_values!(adr , "ADR" , Adr); + make_getter_function_for_optional!(anniversary , "ANNIVERSARY" , Anniversary); + make_getter_function_for_optional!(bday , "BDAY" , BDay); + make_getter_function_for_values!(categories , "CATEGORIES" , Category); + make_getter_function_for_optional!(clientpidmap , "CLIENTPIDMAP" , ClientPidMap); + make_getter_function_for_values!(email , "EMAIL" , Email); + make_getter_function_for_values!(fullname , "FN" , FullName); + make_getter_function_for_optional!(gender , "GENDER" , Gender); + make_getter_function_for_values!(geo , "GEO" , Geo); + make_getter_function_for_values!(impp , "IMPP" , IMPP); + make_getter_function_for_values!(key , "KEY" , Key); + make_getter_function_for_values!(lang , "LANG" , Lang); + make_getter_function_for_values!(logo , "LOGO" , Logo); + make_getter_function_for_values!(member , "MEMBER" , Member); + make_getter_function_for_optional!(name , "N" , Name); + make_getter_function_for_values!(nickname , "NICKNAME" , NickName); + make_getter_function_for_values!(note , "NOTE" , Note); + make_getter_function_for_values!(org , "ORG" , Organization); + make_getter_function_for_values!(photo , "PHOTO" , Photo); + make_getter_function_for_optional!(proid , "PRIOD" , Proid); + make_getter_function_for_values!(related , "RELATED" , Related); + make_getter_function_for_optional!(rev , "REV" , Rev); + make_getter_function_for_values!(role , "ROLE" , Title); + make_getter_function_for_values!(sound , "SOUND" , Sound); + make_getter_function_for_values!(tel , "TEL" , Tel); + make_getter_function_for_values!(title , "TITLE" , Title); + make_getter_function_for_values!(tz , "TZ" , Tz); + make_getter_function_for_optional!(uid , "UID" , Uid); + make_getter_function_for_values!(url , "URL" , Url); + make_getter_function_for_optional!(version , "VERSION" , Version); + + make_builder_fn!(fn with_adr building "ADR" with_params, + |o| o.unwrap_or(String::from("")) => + pobox : Option<String>, + ext : Option<String>, + street : Option<String>, + locality : Option<String>, + region : Option<String>, + code : Option<String>, + country : Option<String>); + + make_builder_fn!(fn with_anniversary building "ANNIVERSARY" , |o| o => value: String); + make_builder_fn!(fn with_bday building "BDAY" with_params , |o| o => value: String); + make_builder_fn!(fn with_categories building "CATEGORIES" , |o| o.join(";") => org: Vec<String>); + make_builder_fn!(fn with_clientpidmap building "CLIENTPIDMAP" , |o| o => raw: String); + make_builder_fn!(fn with_email building "EMAIL" , |o| o => email: String); + make_builder_fn!(fn with_fullname building "FN" , |o| o => fullname: String); + make_builder_fn!(fn with_gender building "GENDER" with_params , |o| o => value: String); + make_builder_fn!(fn with_geo building "GEO" , |o| o => uri: String); + make_builder_fn!(fn with_impp building "IMPP" , |o| o => uri: String); + make_builder_fn!(fn with_key building "KEY" , |o| o => uri: String); + make_builder_fn!(fn with_lang building "LANG" , |o| o => lang: String); + make_builder_fn!(fn with_logo building "LOGO" , |o| o => uri: String); + make_builder_fn!(fn with_member building "MEMBER" , |o| o => uri: String); + + make_builder_fn!(fn with_name building "N" with_params, + |o| o.unwrap_or(String::from("")) => + surname : Option<String>, + given_name : Option<String>, + additional_name : Option<String>, + honorific_prefixes : Option<String>, + honorific_suffixes : Option<String>); + + make_builder_fn!(fn with_nickname building "NICKNAME" with_params , |o| o => name: String); + make_builder_fn!(fn with_note building "NOTE" , |o| o => text: String); + make_builder_fn!(fn with_org building "ORG" , |o| o.join(";") => org: Vec<String>); + make_builder_fn!(fn with_photo building "PHOTO" with_params , |o| o => param: String); + make_builder_fn!(fn with_proid building "PRODID" , |o| o => param: String); + make_builder_fn!(fn with_related building "RELATED" , |o| o => uri: String); + make_builder_fn!(fn with_rev building "REV" , |o| o => timestamp: String); + make_builder_fn!(fn with_role building "ROLE" , |o| o => role: String); + make_builder_fn!(fn with_sound building "SOUND" , |o| o => uri: String); + make_builder_fn!(fn with_tel building "TEL" with_params , |o| o => value: String); + make_builder_fn!(fn with_title building "TITLE" , |o| o => title: String); + make_builder_fn!(fn with_tz building "TZ" , |o| o => tz: String); + make_builder_fn!(fn with_uid building "UID" , |o| o => uri: String); + make_builder_fn!(fn with_url building "URL" , |o| o => uri: String); + make_builder_fn!(fn with_version building "VERSION" , |o| o => version: String); + +} + +impl Default for Vcard { + fn default() -> Self { + Vcard(Component::new(String::from("VCARD"))) + } +} + +impl Deref for Vcard { + type Target = Component; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +macro_rules! create_data_type { + ( $name:ident ) => { + #[derive(Eq, PartialEq)] + pub struct $name(String, Parameters); + + impl $name { + fn new(raw: String, params: Parameters) -> $name { + $name(raw, params) + } + + pub fn raw(&self) -> &String { + &self.0 + } + } + + impl From<Property> for $name { + fn from(p: Property) -> $name { + $name::new(p.raw_value, p.params) + } + } + } +} + +create_data_type!(Adr); +create_data_type!(Anniversary); +create_data_type!(BDay); +create_data_type!(Category); +create_data_type!(ClientPidMap); +create_data_type!(Email); +create_data_type!(FullName); +create_data_type!(Gender); +create_data_type!(Geo); +create_data_type!(IMPP); +create_data_type!(Key); +create_data_type!(Lang); +create_data_type!(Logo); +create_data_type!(Member); +create_data_type!(Name); +create_data_type!(NickName); +create_data_type!(Note); +create_data_type!(Organization); +create_data_type!(PhoneNumber); +create_data_type!(Photo); +create_data_type!(Proid); +create_data_type!(Related); +create_data_type!(Rev); +create_data_type!(Sound); +create_data_type!(Tel); +create_data_type!(Title); +create_data_type!(Tz); +create_data_type!(Uid); +create_data_type!(Url); +create_data_type!(Version); + +/// A Name type +/// +/// offers functionality to get firstname, middlenames and lastname. +/// +/// The parsing behaviour is implemented in a way that splits at whitespace, following these rules: +/// +/// * If there is only one element after splitting, this is considered the lastname +/// * If there are two elements, this is firstname and lastname +/// * If there are more than two elements, firstname and lastname are the first and last elements +/// respectively, all others are middlenames. +/// +impl Name { + + pub fn plain(&self) -> String { + self.0.clone() + } + + pub fn surname(&self) -> Option<String> { + self.0.split(";").nth(0).map(String::from) + } + + pub fn given_name(&self) -> Option<String> { + self.0.split(";").nth(1).map(String::from) + } + + pub fn additional_names(&self) -> Option<String> { + self.0.split(";").nth(2).map(String::from) + } + + pub fn honorific_prefixes(&self) -> Option<String> { + self.0.split(";").nth(3).map(String::from) + } + + pub fn honorific_suffixes(&self) -> Option<String> { + self.0.split(";").nth(4).map(String::from) + } + + /// Alias for Name::surname() + #[inline] + pub fn family_name(&self) -> Option<String> { + self.surname() + } + +} + +pub struct VcardBuilder(Component); + +#[cfg(test)] +mod test { + use super::Vcard; + + #[test] + fn test_vcard_basic() { + let item = Vcard::build( + "BEGIN:VCARD\n\ + VERSION:2.1\n\ + N:Mustermann;Erika\n\ + FN:Erika Mustermann\n\ + ORG:Wikipedia\n\ + TITLE:Oberleutnant\n\ + PHOTO;JPEG:http://commons.wikimedia.org/wiki/File:Erika_Mustermann_2010.jpg\n\ + TEL;WORK;VOICE:(0221) 9999123\n\n\n\ + TEL;HOME;VOICE:(0221) 1234567\n\ + ADR;HOME:;;Heidestrasse 17;Koeln;;51147;Deutschland\n\ + EMAIL;PREF;INTERNET:erika@mustermann.de\n\ + REV:20140301T221110Z\n\ + END:VCARD\n\r\n\n").unwrap(); + + assert_eq!(item.adr()[0].raw(), ";;Heidestrasse 17;Koeln;;51147;Deutschland"); + assert_eq!(item.fullname()[0].raw(), "Erika Mustermann"); + assert_eq!(item.name().unwrap().plain(), "Mustermann;Erika"); + assert_eq!(item.name().unwrap().surname().unwrap() , "Mustermann"); + assert_eq!(item.name().unwrap().given_name().unwrap() , "Erika"); + assert_eq!(item.org()[0].raw() , "Wikipedia"); + assert_eq!(item.title()[0].raw() , "Oberleutnant"); + } + + #[test] + fn test_vcard_builder() { + use component::write_component; + + let build = Vcard::default() + .with_name(parameters!(), + None, + Some("Mustermann".into()), + None, + Some("Erika".into()), + None) + .with_fullname("Erika Mustermann".into()) + .with_org(vec!["Wikipedia".into()]) + .with_title("Oberleutnant".into()) + .with_tel(parameters!("TYPE" => "WORK"), "(0221) 9999123".into()) + .with_tel(parameters!("TYPE" => "HOME"), "(0221) 1234567".into()) + .with_adr(parameters!("TYPE" => "HOME"), + None, + None, + Some("Heidestrasse 17".into()), + Some("Koeln".into()), + None, + Some("51147".into()), + Some("Deutschland".into())) + .with_email("erika@mustermann.de".into()) + .with_rev("20140301T221110Z".into()); + + let build_string = write_component(&build); + + let expected = + "BEGIN:VCARD\r\n\ + ADR;TYPE=HOME:\\;\\;Heidestrasse 17\\;Koeln\\;\\;51147\\;Deutschland\r\n\ + EMAIL:erika@mustermann.de\r\n\ + FN:Erika Mustermann\r\n\ + N:\\;Mustermann\\;\\;Erika\\;\r\n\ + ORG:Wikipedia\r\n\ + REV:20140301T221110Z\r\n\ + TEL;TYPE=WORK:(0221) 9999123\r\n\ + TEL;TYPE=HOME:(0221) 1234567\r\n\ + TITLE:Oberleutnant\r\n\ + END:VCARD\r\n"; + + + assert_eq!(expected, build_string); + } + +} + |