icann-rdap-common 0.0.29

Common RDAP data structures.
Documentation
//! Convert a Contact to jCard/vCard.
use std::str::FromStr;

use serde_json::{json, Map, Value};

use super::Contact;

impl Contact {
    /// Output the Contact data as vCard in JSON values ([`Vec<Value>`]).
    ///
    /// ```rust
    /// use icann_rdap_common::contact::Contact;
    /// use serde::Serialize;
    /// use serde_json::Value;
    ///
    /// let contact = Contact::builder()
    ///   .kind("individual")
    ///   .full_name("Bob Smurd")
    ///   .build();
    ///
    /// let v = contact.to_vcard();
    /// let json = serde_json::to_string(&v);
    /// ```
    pub fn to_vcard(&self) -> Vec<Value> {
        // start the vcard with the version.
        let mut vcard: Vec<Value> = vec![json!(["version", {}, "text", "4.0"])];

        if let Some(full_name) = &self.full_name() {
            vcard.push(json!(["fn", {}, "text", full_name]));
        }

        if let Some(name_parts) = &self.name_parts() {
            let surnames = vec_string_to_value(&name_parts.surnames);
            let given_names = vec_string_to_value(&name_parts.given_names);
            let middle_names = vec_string_to_value(&name_parts.middle_names);
            let prefixes = vec_string_to_value(&name_parts.prefixes);
            let suffixes = vec_string_to_value(&name_parts.suffixes);
            vcard.push(json!([
                "n",
                {},
                "text",
                [surnames, given_names, middle_names, prefixes, suffixes]
            ]));
        }

        if let Some(kind) = &self.kind {
            vcard.push(json!(["kind", {}, "text", kind]));
        }

        if let Some(langs) = &self.langs {
            for lang in langs {
                let mut params: Map<String, Value> = Map::new();
                if let Some(pref) = lang.preference {
                    params.insert("pref".to_string(), Value::String(pref.to_string()));
                }
                vcard.push(json!([
                    "lang",
                    Value::from(params),
                    "language-tag",
                    lang.tag
                ]))
            }
        }

        for org_name in self.organization_names() {
            vcard.push(json!(["org", {}, "text", org_name]));
        }

        for title in self.titles() {
            vcard.push(json!(["title", {}, "text", title]));
        }

        for role in self.roles() {
            vcard.push(json!(["role", {}, "text", role]));
        }

        for nick_name in self.nick_names() {
            vcard.push(json!(["nickname", {}, "text", nick_name]));
        }

        if let Some(emails) = &self.emails {
            for email in emails {
                let mut params: Map<String, Value> = Map::new();
                if let Some(pref) = email.preference {
                    params.insert("pref".to_string(), Value::String(pref.to_string()));
                }
                if let Some(contexts) = email.contexts.as_ref() {
                    params.insert("type".to_string(), vec_string_to_param(contexts));
                }
                vcard.push(json!(["email", Value::from(params), "text", email.email]))
            }
        }

        if let Some(phones) = &self.phones {
            for phone in phones {
                let mut params: Map<String, Value> = Map::new();
                if let Some(pref) = phone.preference {
                    params.insert("pref".to_string(), Value::String(pref.to_string()));
                }
                let mut types: Vec<String> = vec![];
                if let Some(contexts) = &phone.contexts {
                    types.append(&mut contexts.clone());
                }
                if let Some(features) = &phone.features {
                    types.append(&mut features.clone());
                }
                params.insert("type".to_string(), vec_string_to_param(&types));
                vcard.push(json!(["tel", Value::from(params), "text", phone.phone]))
            }
        }

        for addr in self.postal_addresses() {
            let mut params: Map<String, Value> = Map::new();
            if let Some(pref) = addr.preference {
                params.insert("pref".to_string(), Value::String(pref.to_string()));
            }
            if let Some(contexts) = addr.contexts.as_ref() {
                params.insert("type".to_string(), vec_string_to_param(contexts));
            }
            if let Some(full_address) = &addr.full_address {
                params.insert(
                    "label".to_string(),
                    Value::from_str(full_address).expect("serializing full address"),
                );
            }
            let mut lines: Vec<String> = vec![];
            if let Some(street_parts) = &addr.street_parts {
                lines.push(street_parts.first().cloned().unwrap_or("".to_string()));
                lines.push(street_parts.get(1).cloned().unwrap_or("".to_string()));
                lines.push(street_parts.get(2).cloned().unwrap_or("".to_string()));
            } else {
                lines.push("".to_string());
                lines.push("".to_string());
                lines.push("".to_string());
            }
            if let Some(locality) = &addr.locality {
                lines.push(locality.to_owned());
            } else {
                lines.push("".to_string());
            }
            if let Some(region_name) = &addr.region_name {
                lines.push(region_name.to_owned());
            } else if let Some(region_code) = &addr.region_code {
                lines.push(region_code.to_owned());
            } else {
                lines.push("".to_string());
            }
            if let Some(postal_code) = &addr.postal_code {
                lines.push(postal_code.to_owned());
            } else {
                lines.push("".to_string());
            }
            if let Some(country_name) = &addr.country_name {
                lines.push(country_name.to_owned());
            } else {
                lines.push("".to_string());
            }
            if let Some(country_code) = &addr.country_code {
                params.insert("cc".to_string(), Value::String(country_code.to_string()));
            }
            vcard.push(json!(["adr", Value::from(params), "text", lines]))
        }

        if let Some(contact_uris) = &self.contact_uris {
            for uri in contact_uris {
                vcard.push(json!(["contact-uri", {}, "uri", uri]));
            }
        }

        if let Some(urls) = &self.urls {
            for url in urls {
                vcard.push(json!(["url", {}, "uri", url]));
            }
        }

        // return the vcard array
        vec![Value::String("vcard".to_string()), Value::from(vcard)]
    }
}

fn vec_string_to_value(strings: &Option<Vec<String>>) -> Value {
    let Some(strings) = strings else {
        return Value::String("".to_string());
    };

    if strings.is_empty() {
        return Value::String("".to_string());
    };

    if strings.len() == 1 {
        let Some(one) = strings.first() else {
            panic!("couldn't get first element on length of 1")
        };
        return Value::String(one.to_owned());
    };

    // else
    Value::from(strings.clone())
}

fn vec_string_to_param(strings: &[String]) -> Value {
    if strings.is_empty() {
        return Value::String("".to_string());
    };

    if strings.len() == 1 {
        let Some(one) = strings.first() else {
            panic!("couldn't get first element on length of 1")
        };
        return Value::String(one.to_owned());
    };

    // else
    Value::from(strings)
}

#[cfg(test)]
mod tests {

    use crate::contact::{Contact, Email, Lang, NameParts, Phone, PostalAddress};

    #[test]
    fn test_contact_vcard_round_trip() {
        // GIVEN
        let contact = Contact::builder()
            .full_name("Joe User")
            .name_parts(
                NameParts::builder()
                    .surnames(vec!["User".to_string()])
                    .given_names(vec!["Joe".to_string()])
                    .suffixes(vec!["ing. jr".to_string(), "M.Sc.".to_string()])
                    .build(),
            )
            .kind("individual")
            .langs(vec![
                Lang::builder().preference(1).tag("fr").build(),
                Lang::builder().preference(2).tag("en").build(),
            ])
            .organization_names(vec!["Example".to_string()])
            .titles(vec!["Research Scientist".to_string()])
            .roles(vec!["Project Lead".to_string()])
            .contact_uris(vec!["https://example.com/contact-form".to_string()])
            .postal_addresses(vec![PostalAddress::builder()
                .context("work")
                .country_code("CA")
                .country_name("Canada")
                .postal_code("G1V 2M2")
                .region_code("QC")
                .locality("Quebec")
                .street_parts(vec![
                    "Suite 1234".to_string(),
                    "4321 Rue Somewhere".to_string(),
                ])
                .build()])
            .phones(vec![
                Phone::builder()
                    .preference(1)
                    .contexts(vec!["work".to_string()])
                    .features(vec!["voice".to_string()])
                    .phone("tel:+1-555-555-1234;ext=102")
                    .build(),
                Phone::builder()
                    .contexts(vec!["work".to_string(), "cell".to_string()])
                    .features(vec![
                        "voice".to_string(),
                        "video".to_string(),
                        "text".to_string(),
                    ])
                    .phone("tel:+1-555-555-4321")
                    .build(),
            ])
            .emails(vec![Email::builder()
                .contexts(vec!["work".to_string()])
                .email("joe.user@example.com")
                .build()])
            .urls(vec!["https://example.com/some-url".to_string()])
            .build();

        // WHEN
        let actual = Contact::from_vcard(&contact.to_vcard()).expect("from vcard");

        // THEN
        //   yes, contact implements Eq, but by breaking everything done
        //   we can find where problems lie faster.
        assert_eq!(contact.full_name(), actual.full_name());
        assert_eq!(contact.name_parts(), actual.name_parts());
        assert_eq!(contact.kind, actual.kind);
        assert_eq!(contact.langs, actual.langs);
        assert_eq!(contact.organization_names(), actual.organization_names());
        assert_eq!(contact.titles(), actual.titles());
        assert_eq!(contact.roles(), actual.roles());
        assert_eq!(contact.postal_addresses(), actual.postal_addresses());
        assert_eq!(contact.phones, actual.phones);
        assert_eq!(contact.emails, actual.emails);
        assert_eq!(contact.contact_uris, actual.contact_uris);
        assert_eq!(contact.urls, actual.urls);
        //   but just in case we forgot something
        assert_eq!(contact, actual);
    }
}