846 lines
28 KiB
Rust
846 lines
28 KiB
Rust
//! 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<Value> = serde_json::from_str(json).unwrap();
|
|
/// let contact = Contact::from_vcard(&data);
|
|
/// ```
|
|
pub fn from_vcard(vcard_array: &[Value]) -> Option<Contact> {
|
|
// 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<Value>>;
|
|
}
|
|
|
|
impl<'a> FindProperty<'a> for &'a [Value] {
|
|
fn find_property(self, name: &'a str) -> Option<&'a Vec<Value>> {
|
|
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<Value>>;
|
|
}
|
|
|
|
impl<'a> FindProperties<'a> for &'a [Value] {
|
|
fn find_properties(self, name: &'a str) -> Vec<&'a Vec<Value>> {
|
|
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<String>;
|
|
}
|
|
|
|
impl<'a> GetText<'a> for Option<&'a Vec<Value>> {
|
|
fn get_text(self) -> Option<String> {
|
|
let values = self?;
|
|
let fourth = values.get(3)?;
|
|
fourth.as_str().map(|s| s.to_owned())
|
|
}
|
|
}
|
|
|
|
impl<'a> GetText<'a> for &'a Vec<Value> {
|
|
fn get_text(self) -> Option<String> {
|
|
let fourth = self.get(3)?;
|
|
fourth.as_str().map(|s| s.to_owned())
|
|
}
|
|
}
|
|
|
|
trait GetTexts<'a> {
|
|
fn get_texts(self) -> Option<Vec<String>>;
|
|
}
|
|
|
|
impl<'a> GetTexts<'a> for &'a [&'a Vec<Value>] {
|
|
fn get_texts(self) -> Option<Vec<String>> {
|
|
let texts = self
|
|
.iter()
|
|
.filter_map(|prop| (*prop).get_text())
|
|
.collect::<Vec<String>>();
|
|
(!texts.is_empty()).then_some(texts)
|
|
}
|
|
}
|
|
|
|
trait GetPreference<'a> {
|
|
fn get_preference(self) -> Option<u64>;
|
|
}
|
|
|
|
impl<'a> GetPreference<'a> for &'a Vec<Value> {
|
|
fn get_preference(self) -> Option<u64> {
|
|
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<String>;
|
|
}
|
|
|
|
impl<'a> GetLabel<'a> for &'a Vec<Value> {
|
|
fn get_label(self) -> Option<String> {
|
|
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<Vec<String>>;
|
|
}
|
|
|
|
impl<'a> GetContexts<'a> for &'a Vec<Value> {
|
|
fn get_contexts(self) -> Option<Vec<String>> {
|
|
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::<Vec<String>>();
|
|
(!contexts.is_empty()).then_some(contexts)
|
|
}
|
|
}
|
|
|
|
trait GetFeatures<'a> {
|
|
fn get_features(self) -> Option<Vec<String>>;
|
|
}
|
|
|
|
impl<'a> GetFeatures<'a> for &'a Vec<Value> {
|
|
fn get_features(self) -> Option<Vec<String>> {
|
|
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::<Vec<String>>();
|
|
(!features.is_empty()).then_some(features)
|
|
}
|
|
}
|
|
|
|
trait GetLangs<'a> {
|
|
fn get_langs(self) -> Option<Vec<Lang>>;
|
|
}
|
|
|
|
impl<'a> GetLangs<'a> for &'a [&'a Vec<Value>] {
|
|
fn get_langs(self) -> Option<Vec<Lang>> {
|
|
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::<Vec<Lang>>();
|
|
(!langs.is_empty()).then_some(langs)
|
|
}
|
|
}
|
|
|
|
trait GetEmails<'a> {
|
|
fn get_emails(self) -> Option<Vec<Email>>;
|
|
}
|
|
|
|
impl<'a> GetEmails<'a> for &'a [&'a Vec<Value>] {
|
|
fn get_emails(self) -> Option<Vec<Email>> {
|
|
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::<Vec<Email>>();
|
|
(!emails.is_empty()).then_some(emails)
|
|
}
|
|
}
|
|
|
|
trait GetPhones<'a> {
|
|
fn get_phones(self) -> Option<Vec<Phone>>;
|
|
}
|
|
|
|
impl<'a> GetPhones<'a> for &'a [&'a Vec<Value>] {
|
|
fn get_phones(self) -> Option<Vec<Phone>> {
|
|
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::<Vec<Phone>>();
|
|
(!phones.is_empty()).then_some(phones)
|
|
}
|
|
}
|
|
|
|
trait GetPostalAddresses<'a> {
|
|
fn get_postal_addresses(self) -> Option<Vec<PostalAddress>>;
|
|
}
|
|
|
|
impl<'a> GetPostalAddresses<'a> for &'a [&'a Vec<Value>] {
|
|
fn get_postal_addresses(self) -> Option<Vec<PostalAddress>> {
|
|
let addrs = self
|
|
.iter()
|
|
.map(|prop| {
|
|
let mut postal_code: Option<String> = None;
|
|
let mut country_code: Option<String> = None;
|
|
let mut country_name: Option<String> = None;
|
|
let mut region_code: Option<String> = None;
|
|
let mut region_name: Option<String> = None;
|
|
let mut locality: Option<String> = None;
|
|
let mut street_parts: Vec<String> = 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::<Vec<PostalAddress>>();
|
|
(!addrs.is_empty()).then_some(addrs)
|
|
}
|
|
}
|
|
|
|
trait GetNameParts<'a> {
|
|
fn get_name_parts(self) -> Option<NameParts>;
|
|
}
|
|
|
|
impl<'a> GetNameParts<'a> for Option<&'a Vec<Value>> {
|
|
fn get_name_parts(self) -> Option<NameParts> {
|
|
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<Vec<String>> = None;
|
|
let mut surnames: Option<Vec<String>> = None;
|
|
let mut given_names: Option<Vec<String>> = None;
|
|
let mut middle_names: Option<Vec<String>> = None;
|
|
let mut suffixes: Option<Vec<String>> = 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<Vec<String>> {
|
|
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::<Vec<String>>();
|
|
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::<Vec<Value>>(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::<Vec<Value>>(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");
|
|
}
|
|
}
|