//! Convert jCard/vCard to Contact. use serde_json::Value; use super::{Contact, Email, Lang, NameParts, Phone, PostalAddress}; impl Contact { /// Creates a Contact from an array of [`Value`]s. /// /// ```rust /// use icann_rdap_common::contact::Contact; /// use serde::Deserialize; /// use serde_json::Value; /// /// let json = r#" /// [ /// "vcard", /// [ /// ["version", {}, "text", "4.0"], /// ["fn", {}, "text", "Joe User"], /// ["kind", {}, "text", "individual"], /// ["org", { /// "type":"work" /// }, "text", "Example"], /// ["title", {}, "text", "Research Scientist"], /// ["role", {}, "text", "Project Lead"], /// ["adr", /// { "type":"work" }, /// "text", /// [ /// "", /// "Suite 1234", /// "4321 Rue Somewhere", /// "Quebec", /// "QC", /// "G1V 2M2", /// "Canada" /// ] /// ], /// ["tel", /// { "type":["work", "voice"], "pref":"1" }, /// "uri", "tel:+1-555-555-1234;ext=102" /// ], /// ["email", /// { "type":"work" }, /// "text", "joe.user@example.com" /// ] /// ] /// ]"#; /// /// let data: Vec = serde_json::from_str(json).unwrap(); /// let contact = Contact::from_vcard(&data); /// ``` pub fn from_vcard(vcard_array: &[Value]) -> Option { // value should be "vcard" followed by array let value = vcard_array.first()?; let vcard_literal = value.as_str()?; if !vcard_literal.eq_ignore_ascii_case("vcard") { return None; }; let vcard = vcard_array.get(1)?; let vcard = vcard.as_array()?; let contact = Contact::builder() .and_full_name(vcard.find_property("fn").get_text()) .and_kind(vcard.find_property("kind").get_text()) .titles(vcard.find_properties("title").get_texts().unwrap_or(vec![])) .roles(vcard.find_properties("role").get_texts().unwrap_or(vec![])) .nick_names( vcard .find_properties("nickname") .get_texts() .unwrap_or(vec![]), ) .organization_names(vcard.find_properties("org").get_texts().unwrap_or(vec![])) .langs(vcard.find_properties("lang").get_langs().unwrap_or(vec![])) .emails( vcard .find_properties("email") .get_emails() .unwrap_or(vec![]), ) .phones(vcard.find_properties("tel").get_phones().unwrap_or(vec![])) .postal_addresses( vcard .find_properties("adr") .get_postal_addresses() .unwrap_or(vec![]), ) .and_name_parts(vcard.find_property("n").get_name_parts()) .contact_uris( vcard .find_properties("contact-uri") .get_texts() .unwrap_or(vec![]), ) .urls(vcard.find_properties("url").get_texts().unwrap_or(vec![])) .build(); contact.is_non_empty().then_some(contact) } } trait FindProperty<'a> { fn find_property(self, name: &'a str) -> Option<&'a Vec>; } impl<'a> FindProperty<'a> for &'a [Value] { fn find_property(self, name: &'a str) -> Option<&'a Vec> { self.iter() .filter_map(|prop_array| prop_array.as_array()) .find(|prop_array| { if let Some(prop_name) = prop_array.first() { if let Some(prop_name) = prop_name.as_str() { prop_name.eq_ignore_ascii_case(name) } else { false } } else { false } }) } } trait FindProperties<'a> { fn find_properties(self, name: &'a str) -> Vec<&'a Vec>; } impl<'a> FindProperties<'a> for &'a [Value] { fn find_properties(self, name: &'a str) -> Vec<&'a Vec> { self.iter() .filter_map(|prop_array| prop_array.as_array()) .filter(|prop_array| { if let Some(prop_name) = prop_array.first() { if let Some(prop_name) = prop_name.as_str() { prop_name.eq_ignore_ascii_case(name) } else { false } } else { false } }) .collect() } } trait GetText<'a> { fn get_text(self) -> Option; } impl<'a> GetText<'a> for Option<&'a Vec> { fn get_text(self) -> Option { let values = self?; let fourth = values.get(3)?; fourth.as_str().map(|s| s.to_owned()) } } impl<'a> GetText<'a> for &'a Vec { fn get_text(self) -> Option { let fourth = self.get(3)?; fourth.as_str().map(|s| s.to_owned()) } } trait GetTexts<'a> { fn get_texts(self) -> Option>; } impl<'a> GetTexts<'a> for &'a [&'a Vec] { fn get_texts(self) -> Option> { let texts = self .iter() .filter_map(|prop| (*prop).get_text()) .collect::>(); (!texts.is_empty()).then_some(texts) } } trait GetPreference<'a> { fn get_preference(self) -> Option; } impl<'a> GetPreference<'a> for &'a Vec { fn get_preference(self) -> Option { let second = self.get(1)?; let second = second.as_object()?; let preference = second.get("pref")?; preference.as_str().and_then(|s| s.parse().ok()) } } trait GetLabel<'a> { fn get_label(self) -> Option; } impl<'a> GetLabel<'a> for &'a Vec { fn get_label(self) -> Option { let second = self.get(1)?; let second = second.as_object()?; let label = second.get("label")?; label.as_str().map(|s| s.to_owned()) } } const CONTEXTS: [&str; 6] = ["home", "work", "office", "private", "mobile", "cell"]; trait GetContexts<'a> { fn get_contexts(self) -> Option>; } impl<'a> GetContexts<'a> for &'a Vec { fn get_contexts(self) -> Option> { let second = self.get(1)?; let second = second.as_object()?; let contexts = second.get("type")?; if let Some(context) = contexts.as_str() { let context = context.to_lowercase(); if CONTEXTS.contains(&context.as_str()) { return Some(vec![context]); } else { return None; } }; let contexts = contexts.as_array()?; let contexts = contexts .iter() .filter_map(|v| v.as_str()) .map(|s| s.to_lowercase()) .filter(|s| CONTEXTS.contains(&s.as_str())) .collect::>(); (!contexts.is_empty()).then_some(contexts) } } trait GetFeatures<'a> { fn get_features(self) -> Option>; } impl<'a> GetFeatures<'a> for &'a Vec { fn get_features(self) -> Option> { let second = self.get(1)?; let second = second.as_object()?; let features = second.get("type")?; if let Some(feature) = features.as_str() { let feature = feature.to_lowercase(); if !CONTEXTS.contains(&feature.as_str()) { return Some(vec![feature]); } else { return None; } }; let features = features.as_array()?; let features = features .iter() .filter_map(|v| v.as_str()) .map(|s| s.to_lowercase()) .filter(|s| !CONTEXTS.contains(&s.as_str())) .collect::>(); (!features.is_empty()).then_some(features) } } trait GetLangs<'a> { fn get_langs(self) -> Option>; } impl<'a> GetLangs<'a> for &'a [&'a Vec] { fn get_langs(self) -> Option> { let langs = self .iter() .filter_map(|prop| { let tag = (*prop).get_text()?; let lang = Lang::builder() .tag(tag) .and_preference((*prop).get_preference()) .build(); Some(lang) }) .collect::>(); (!langs.is_empty()).then_some(langs) } } trait GetEmails<'a> { fn get_emails(self) -> Option>; } impl<'a> GetEmails<'a> for &'a [&'a Vec] { fn get_emails(self) -> Option> { let emails = self .iter() .filter_map(|prop| { let addr = (*prop).get_text()?; let email = Email::builder() .email(addr) .contexts((*prop).get_contexts().unwrap_or_default()) .and_preference((*prop).get_preference()) .build(); Some(email) }) .collect::>(); (!emails.is_empty()).then_some(emails) } } trait GetPhones<'a> { fn get_phones(self) -> Option>; } impl<'a> GetPhones<'a> for &'a [&'a Vec] { fn get_phones(self) -> Option> { let phones = self .iter() .filter_map(|prop| { let number = (*prop).get_text()?; let phone = Phone::builder() .phone(number) .features((*prop).get_features().unwrap_or_default()) .contexts((*prop).get_contexts().unwrap_or_default()) .and_preference((*prop).get_preference()) .build(); Some(phone) }) .collect::>(); (!phones.is_empty()).then_some(phones) } } trait GetPostalAddresses<'a> { fn get_postal_addresses(self) -> Option>; } impl<'a> GetPostalAddresses<'a> for &'a [&'a Vec] { fn get_postal_addresses(self) -> Option> { let addrs = self .iter() .map(|prop| { let mut postal_code: Option = None; let mut country_code: Option = None; let mut country_name: Option = None; let mut region_code: Option = None; let mut region_name: Option = None; let mut locality: Option = None; let mut street_parts: Vec = vec![]; if let Some(fourth) = prop.get(3) { if let Some(addr) = fourth.as_array() { // the jcard address fields are in a different index of the array. // // [ // "adr", // {}, // "text", // [ // "Mail Stop 3", // post office box (not recommended for use) // "Suite 3000", // apartment or suite (not recommended for use) // "123 Maple Ave", // street address // "Quebec", // locality or city name // "QC", // region (can be either a code or full name) // "G1V 2M2", // postal code // "Canada" // full country name // ] // ], if let Some(pobox) = addr.first() { if let Some(s) = pobox.as_str() { if !s.is_empty() { street_parts.push(s.to_string()) } } } if let Some(appt) = addr.get(1) { if let Some(s) = appt.as_str() { if !s.is_empty() { street_parts.push(s.to_string()) } } } if let Some(street) = addr.get(2) { if let Some(s) = street.as_str() { if !s.is_empty() { street_parts.push(s.to_string()) } } else if let Some(arry_s) = street.as_array() { arry_s .iter() .filter_map(|v| v.as_str()) .filter(|s| !s.is_empty()) .for_each(|s| street_parts.push(s.to_string())) } } if let Some(city) = addr.get(3) { if let Some(s) = city.as_str() { if !s.is_empty() { locality = Some(s.to_string()); } } } if let Some(region) = addr.get(4) { if let Some(s) = region.as_str() { if !s.is_empty() { if s.len() == 2 && s.to_uppercase() == s { region_code = Some(s.to_string()) } else { region_name = Some(s.to_string()) } } } } if let Some(pc) = addr.get(5) { if let Some(s) = pc.as_str() { if !s.is_empty() { postal_code = Some(s.to_string()); } } } if let Some(country) = addr.get(6) { if let Some(s) = country.as_str() { if !s.is_empty() { if s.len() == 2 && s.to_uppercase() == s { country_code = Some(s.to_string()) } else { country_name = Some(s.to_string()) } } } } } }; let street_parts = (!street_parts.is_empty()).then_some(street_parts); PostalAddress::builder() .and_full_address((*prop).get_label()) .contexts((*prop).get_contexts().unwrap_or_default()) .and_preference((*prop).get_preference()) .and_country_code(country_code) .and_country_name(country_name) .and_postal_code(postal_code) .and_region_name(region_name) .and_region_code(region_code) .and_locality(locality) .street_parts(street_parts.unwrap_or_default()) .build() }) .collect::>(); (!addrs.is_empty()).then_some(addrs) } } trait GetNameParts<'a> { fn get_name_parts(self) -> Option; } impl<'a> GetNameParts<'a> for Option<&'a Vec> { fn get_name_parts(self) -> Option { let values = self?; let fourth = values.get(3)?; let parts = fourth.as_array()?; let mut iter = parts.iter().filter(|p| p.is_string() || p.is_array()); let mut prefixes: Option> = None; let mut surnames: Option> = None; let mut given_names: Option> = None; let mut middle_names: Option> = None; let mut suffixes: Option> = None; if let Some(e) = iter.next() { surnames = get_string_or_vec(e); }; if let Some(e) = iter.next() { given_names = get_string_or_vec(e); }; if let Some(e) = iter.next() { middle_names = get_string_or_vec(e); }; if let Some(e) = iter.next() { prefixes = get_string_or_vec(e); }; if let Some(e) = iter.next() { suffixes = get_string_or_vec(e); }; let name_parts = NameParts::builder() .surnames(surnames.unwrap_or_default()) .prefixes(prefixes.unwrap_or_default()) .given_names(given_names.unwrap_or_default()) .middle_names(middle_names.unwrap_or_default()) .suffixes(suffixes.unwrap_or_default()) .build(); if name_parts.surnames.is_none() && name_parts.given_names.is_none() && name_parts.middle_names.is_none() && name_parts.suffixes.is_none() && name_parts.prefixes.is_none() { None } else { Some(name_parts) } } } fn get_string_or_vec(value: &Value) -> Option> { if let Some(e) = value.as_str() { if e.is_empty() { return None; } else { return Some(vec![e.to_string()]); }; }; if let Some(e) = value.as_array() { let strings = e .iter() .filter_map(|e| e.as_str()) .filter(|s| !s.is_empty()) .map(|s| s.to_string()) .collect::>(); return (!strings.is_empty()).then_some(strings); }; None } #[cfg(test)] #[allow(non_snake_case)] mod tests { use serde_json::Value; use crate::contact::{Contact, NameParts}; #[test] fn GIVEN_vcard_WHEN_from_vcard_THEN_properties_are_correct() { // GIVEN let vcard = r#" [ "vcard", [ ["version", {}, "text", "4.0"], ["fn", {}, "text", "Joe User"], ["n", {}, "text", ["User", "Joe", "", "", ["ing. jr", "M.Sc."]] ], ["kind", {}, "text", "individual"], ["lang", { "pref":"1" }, "language-tag", "fr"], ["lang", { "pref":"2" }, "language-tag", "en"], ["org", { "type":"work" }, "text", "Example"], ["title", {}, "text", "Research Scientist"], ["role", {}, "text", "Project Lead"], ["adr", { "type":"work" }, "text", [ "", "Suite 1234", "4321 Rue Somewhere", "Quebec", "QC", "G1V 2M2", "Canada" ] ], ["adr", { "type":"home", "label":"123 Maple Ave\nSuite 90001\nVancouver\nBC\n1239\n" }, "text", [ "", "", "", "", "", "", "" ] ], ["tel", { "type":["work", "voice"], "pref":"1" }, "uri", "tel:+1-555-555-1234;ext=102" ], ["tel", { "type":["work", "cell", "voice", "video", "text"] }, "uri", "tel:+1-555-555-4321" ], ["email", { "type":"work" }, "text", "joe.user@example.com" ], ["geo", { "type":"work" }, "uri", "geo:46.772673,-71.282945"], ["key", { "type":"work" }, "uri", "https://www.example.com/joe.user/joe.asc" ], ["tz", {}, "utc-offset", "-05:00"], ["contact-uri", {}, "uri", "https://example.com/contact-form" ], ["url", {}, "uri", "https://example.com/some-url" ] ] ] "#; // WHEN let actual = serde_json::from_str::>(vcard); // THEN let actual = actual.expect("parsing vcard"); let actual = Contact::from_vcard(&actual).expect("vcard not found"); // full name assert_eq!(actual.full_name.expect("full_name not found"), "Joe User"); // kind assert_eq!(actual.kind.expect("kind not found"), "individual"); // titles assert_eq!( actual .titles .expect("no titles") .first() .expect("titles empty"), "Research Scientist" ); // roles assert_eq!( actual .roles .expect("no roles") .first() .expect("roles empty"), "Project Lead" ); // organization names assert_eq!( actual .organization_names .expect("no organization_names") .first() .expect("organization_names empty"), "Example" ); // nick names assert!(actual.nick_names.is_none()); // langs let Some(langs) = actual.langs else { panic!("langs not found") }; assert_eq!(langs.len(), 2); assert_eq!(langs.first().expect("first lang").tag, "fr"); assert_eq!(langs.first().expect("first lang").preference, Some(1)); assert_eq!(langs.get(1).expect("second lang").tag, "en"); assert_eq!(langs.get(1).expect("second lang").preference, Some(2)); // emails let Some(emails) = actual.emails else { panic!("emails not found") }; let Some(email) = emails.first() else { panic!("no email found") }; assert_eq!(email.email, "joe.user@example.com"); assert!(email .contexts .as_ref() .expect("contexts not found") .contains(&"work".to_string())); // phones let Some(phones) = actual.phones else { panic!("no phones found") }; let Some(phone) = phones.first() else { panic!("no first phone") }; assert_eq!(phone.phone, "tel:+1-555-555-1234;ext=102"); assert!(phone .contexts .as_ref() .expect("no contexts") .contains(&"work".to_string())); assert!(phone .features .as_ref() .expect("no features") .contains(&"voice".to_string())); let Some(phone) = phones.last() else { panic!("no last phone") }; assert_eq!(phone.phone, "tel:+1-555-555-4321"); assert!(phone .contexts .as_ref() .expect("no contexts") .contains(&"cell".to_string())); assert!(phone .features .as_ref() .expect("no features") .contains(&"video".to_string())); // postal addresses let Some(addresses) = actual.postal_addresses else { panic!("no postal addresses") }; let Some(addr) = addresses.first() else { panic!("first address not found") }; assert!(addr .contexts .as_ref() .expect("no contexts") .contains(&"work".to_string())); let Some(street_parts) = &addr.street_parts else { panic!("no street parts") }; assert_eq!(street_parts.first().expect("street part 0"), "Suite 1234"); assert_eq!( street_parts.get(1).expect("street part 1"), "4321 Rue Somewhere" ); assert_eq!(addr.country_name.as_ref().expect("country name"), "Canada"); assert!(addr.country_code.is_none()); assert_eq!(addr.region_code.as_ref().expect("region code"), "QC"); assert!(addr.region_name.is_none()); assert_eq!(addr.postal_code.as_ref().expect("postal code"), "G1V 2M2"); let Some(addr) = addresses.last() else { panic!("last address not found") }; assert!(addr .contexts .as_ref() .expect("no contexts") .contains(&"home".to_string())); assert_eq!( addr.full_address.as_ref().expect("full address not foudn"), "123 Maple Ave\nSuite 90001\nVancouver\nBC\n1239\n" ); // name parts let Some(name_parts) = actual.name_parts else { panic!("no name parts") }; let expected = NameParts::builder() .surnames(vec!["User".to_string()]) .given_names(vec!["Joe".to_string()]) .suffixes(vec!["ing. jr".to_string(), "M.Sc.".to_string()]) .build(); assert_eq!(name_parts, expected); // contact-uris assert_eq!( actual .contact_uris .expect("no contact-uris") .first() .expect("contact-uris empty"), "https://example.com/contact-form" ); // urls assert_eq!( actual .urls .expect("no urls") .first() .expect("urls are empty"), "https://example.com/some-url" ); } #[test] fn GIVEN_vcard_with_addr_street_array_WHEN_from_vcard_THEN_properties_are_correct() { // GIVEN let vcard = r#" [ "vcard", [ ["version", {}, "text", "4.0"], ["fn", {}, "text", "Joe User"], ["adr", { "type":"work" }, "text", [ "", "Suite 1234", ["4321 Rue Blue", "1, Gawwn"], "Quebec", "QC", "G1V 2M2", "Canada" ] ] ] ] "#; // WHEN let actual = serde_json::from_str::>(vcard); // THEN let actual = actual.expect("parsing vcard"); let actual = Contact::from_vcard(&actual).expect("vcard not found"); // full name assert_eq!(actual.full_name.expect("full_name not found"), "Joe User"); // postal addresses let Some(addresses) = actual.postal_addresses else { panic!("no postal addresses") }; let Some(addr) = addresses.first() else { panic!("first address not found") }; assert!(addr .contexts .as_ref() .expect("no contexts") .contains(&"work".to_string())); let Some(street_parts) = &addr.street_parts else { panic!("no street parts") }; assert_eq!(street_parts.first().expect("street part 0"), "Suite 1234"); assert_eq!(street_parts.get(1).expect("street part 1"), "4321 Rue Blue"); assert_eq!(street_parts.get(2).expect("street part 2"), "1, Gawwn"); assert_eq!(addr.country_name.as_ref().expect("country name"), "Canada"); assert!(addr.country_code.is_none()); assert_eq!(addr.region_code.as_ref().expect("region code"), "QC"); assert!(addr.region_name.is_none()); assert_eq!(addr.postal_code.as_ref().expect("postal code"), "G1V 2M2"); } }