bitwarden_exporters/cxf/
identity.rs

1use credential_exchange_format::{
2    AddressCredential, DriversLicenseCredential, EditableField, EditableFieldString,
3    IdentityDocumentCredential, PassportCredential, PersonNameCredential,
4};
5
6use crate::{cxf::editable_field::create_field, Field, Identity};
7
8/// Convert address credentials to Identity (no custom fields needed for address)
9/// According to the mapping specification:
10/// - streetAddress: EditableField<"string"> → Identity::address1
11/// - city: EditableField<"string"> → Identity::city
12/// - territory: EditableField<"subdivision-code"> → Identity::state
13/// - country: EditableField<"country-code"> → Identity::country
14/// - tel: EditableField<"string"> → Identity::phone
15/// - postalCode: EditableField<"string"> → Identity::postal_code
16pub(super) fn address_to_identity(address: AddressCredential) -> (Identity, Vec<Field>) {
17    let identity = Identity {
18        address1: address.street_address.map(Into::into),
19        city: address.city.map(Into::into),
20        state: address.territory.map(Into::into),
21        postal_code: address.postal_code.map(Into::into),
22        country: address.country.map(Into::into),
23        phone: address.tel.map(Into::into),
24        ..Default::default()
25    };
26
27    (identity, vec![])
28}
29
30/// Convert passport credentials to Identity and custom fields
31/// According to CXF mapping document:
32/// - passportNumber: EditableField<"string"> → Identity::passport_number
33/// - nationalIdentificationNumber: EditableField<"string"> → Identity::ssn
34/// - fullName: EditableField<"string"> → Identity::first_name + last_name (split)
35/// - All other fields → CustomFields
36pub(super) fn passport_to_identity(passport: PassportCredential) -> (Identity, Vec<Field>) {
37    // Split full name into first and last name if available
38    let (first_name, last_name) = split_name(&passport.full_name);
39
40    let identity = Identity {
41        first_name,
42        last_name,
43        // Map nationalIdentificationNumber to ssn as closest available field
44        ssn: passport.national_identification_number.map(Into::into),
45        passport_number: passport.passport_number.map(Into::into),
46        ..Default::default()
47    };
48
49    // Create custom fields for unmapped data according to CXF mapping document
50    let custom_fields = [
51        passport
52            .issuing_country
53            .map(|issuing_country| create_field("Issuing Country", &issuing_country)),
54        passport
55            .nationality
56            .map(|nationality| create_field("Nationality", &nationality)),
57        passport
58            .birth_date
59            .map(|birth_date| create_field("Birth Date", &birth_date)),
60        passport
61            .birth_place
62            .map(|birth_place| create_field("Birth Place", &birth_place)),
63        passport.sex.map(|sex| create_field("Sex", &sex)),
64        passport
65            .issue_date
66            .map(|issue_date| create_field("Issue Date", &issue_date)),
67        passport
68            .expiry_date
69            .map(|expiry_date| create_field("Expiry Date", &expiry_date)),
70        passport
71            .issuing_authority
72            .map(|issuing_authority| create_field("Issuing Authority", &issuing_authority)),
73        passport
74            .passport_type
75            .map(|passport_type| create_field("Passport Type", &passport_type)),
76    ]
77    .into_iter()
78    .flatten()
79    .collect();
80
81    (identity, custom_fields)
82}
83
84/// Convert person name credentials to Identity and custom fields
85/// According to CXF mapping:
86/// - title: EditableField<"string"> → Identity::title
87/// - given: EditableField<"string"> → Identity::first_name
88/// - given2: EditableField<"string"> → Identity::middle_name
89/// - surname: EditableField<"string"> → Identity::last_name
90/// - surnamePrefix + surname + surname2: combine for complete last name
91/// - credentials: EditableField<"string"> → Identity::company (as professional credentials)
92/// - Other fields → CustomFields
93pub(super) fn person_name_to_identity(person_name: PersonNameCredential) -> (Identity, Vec<Field>) {
94    // Construct complete last name from surnamePrefix, surname, and surname2
95    let last_name = [
96        person_name.surname_prefix.as_ref(),
97        person_name.surname.as_ref(),
98        person_name.surname2.as_ref(),
99    ]
100    .into_iter()
101    .flatten()
102    .map(|field| field.value.0.clone())
103    .collect::<Vec<_>>()
104    .into_iter()
105    .reduce(|acc, part| format!("{acc} {part}"));
106
107    let identity = Identity {
108        title: person_name.title.map(Into::into),
109        first_name: person_name.given.map(Into::into),
110        middle_name: person_name.given2.map(Into::into),
111        last_name,
112        // Map credentials (e.g., "PhD") to company field as professional qualifications
113        company: person_name.credentials.map(Into::into),
114        ..Default::default()
115    };
116
117    // Create custom fields for unmapped data
118    let custom_fields = [
119        person_name
120            .given_informal
121            .map(|given_informal| create_field("Informal Given Name", &given_informal)),
122        person_name
123            .generation
124            .map(|generation| create_field("Generation", &generation)),
125    ]
126    .into_iter()
127    .flatten()
128    .collect();
129
130    (identity, custom_fields)
131}
132
133/// Convert drivers license credentials to Identity and custom fields
134/// According to CXF mapping document:
135/// - licenseNumber: EditableField<"string"> → Identity::license_number
136/// - fullName: EditableField<"string"> → Identity::first_name + last_name (split)
137/// - territory: EditableField<"subdivision-code"> → Identity::state
138/// - country: EditableField<"country-code"> → Identity::country
139/// - All other fields → CustomFields
140pub(super) fn drivers_license_to_identity(
141    drivers_license: DriversLicenseCredential,
142) -> (Identity, Vec<Field>) {
143    // Split full name into first and last name if available
144    let (first_name, last_name) = split_name(&drivers_license.full_name);
145
146    let identity = Identity {
147        first_name,
148        last_name,
149        // Map territory (state/province) to state field
150        state: drivers_license.territory.map(Into::into),
151        // Map country to country field
152        country: drivers_license.country.map(Into::into),
153        license_number: drivers_license.license_number.map(Into::into),
154        ..Default::default()
155    };
156
157    // Create custom fields for unmapped data according to CXF mapping document
158    let custom_fields = [
159        drivers_license
160            .birth_date
161            .map(|birth_date| create_field("Birth Date", &birth_date)),
162        drivers_license
163            .issue_date
164            .map(|issue_date| create_field("Issue Date", &issue_date)),
165        drivers_license
166            .expiry_date
167            .map(|expiry_date| create_field("Expiry Date", &expiry_date)),
168        drivers_license
169            .issuing_authority
170            .map(|issuing_authority| create_field("Issuing Authority", &issuing_authority)),
171        drivers_license
172            .license_class
173            .map(|license_class| create_field("License Class", &license_class)),
174    ]
175    .into_iter()
176    .flatten()
177    .collect();
178
179    (identity, custom_fields)
180}
181
182/// Convert identity document credentials to Identity and custom fields
183/// According to CXF mapping document: IdentityDocument ↔︎ Identity
184/// Fields are mapped similarly to passport but for general identity documents
185/// - documentNumber: EditableField<"string"> → Identity::passport_number (reusing for general
186///   document number)
187/// - identificationNumber: EditableField<"string"> → Identity::ssn
188/// - fullName: EditableField<"string"> → Identity::first_name + last_name (split)
189/// - All other fields → CustomFields
190pub(super) fn identity_document_to_identity(
191    identity_document: IdentityDocumentCredential,
192) -> (Identity, Vec<Field>) {
193    // Split full name into first and last name if available
194    let (first_name, last_name) = split_name(&identity_document.full_name);
195
196    let identity = Identity {
197        first_name,
198        last_name,
199        // Map identificationNumber to ssn
200        ssn: identity_document.identification_number.map(Into::into),
201        // Map documentNumber to passport_number (reusing for document number)
202        passport_number: identity_document.document_number.map(Into::into),
203        ..Default::default()
204    };
205
206    // Create custom fields for unmapped data according to CXF mapping document
207    let custom_fields = [
208        identity_document
209            .issuing_country
210            .map(|issuing_country| create_field("Issuing Country", &issuing_country)),
211        identity_document
212            .nationality
213            .map(|nationality| create_field("Nationality", &nationality)),
214        identity_document
215            .birth_date
216            .map(|birth_date| create_field("Birth Date", &birth_date)),
217        identity_document
218            .birth_place
219            .map(|birth_place| create_field("Birth Place", &birth_place)),
220        identity_document.sex.map(|sex| create_field("Sex", &sex)),
221        identity_document
222            .issue_date
223            .map(|issue_date| create_field("Issue Date", &issue_date)),
224        identity_document
225            .expiry_date
226            .map(|expiry_date| create_field("Expiry Date", &expiry_date)),
227        identity_document
228            .issuing_authority
229            .map(|issuing_authority| create_field("Issuing Authority", &issuing_authority)),
230    ]
231    .into_iter()
232    .flatten()
233    .collect();
234
235    (identity, custom_fields)
236}
237
238fn split_name(
239    full_name: &Option<EditableField<EditableFieldString>>,
240) -> (Option<String>, Option<String>) {
241    full_name.as_ref().map_or((None, None), |name| {
242        let parts: Vec<&str> = name.value.0.split_whitespace().collect();
243        match parts.as_slice() {
244            [] => (None, None),
245            [first] => (Some(first.to_string()), None),
246            [first, rest @ ..] => (Some(first.to_string()), Some(rest.join(" "))),
247        }
248    })
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    #[test]
256    fn test_split_name_none() {
257        let full_name = None;
258        let (first, last) = split_name(&full_name);
259        assert_eq!(first, None);
260        assert_eq!(last, None);
261    }
262
263    #[test]
264    fn test_split_name_empty_string() {
265        let full_name = Some(EditableField {
266            value: EditableFieldString("".to_string()),
267            label: None,
268            id: None,
269            extensions: None,
270        });
271        let (first, last) = split_name(&full_name);
272        assert_eq!(first, None);
273        assert_eq!(last, None);
274    }
275
276    #[test]
277    fn test_split_name_whitespace_only() {
278        let full_name = Some(EditableField {
279            value: EditableFieldString("   \t\n  ".to_string()),
280            label: None,
281            id: None,
282            extensions: None,
283        });
284        let (first, last) = split_name(&full_name);
285        assert_eq!(first, None);
286        assert_eq!(last, None);
287    }
288
289    #[test]
290    fn test_split_name_single_name() {
291        let full_name = Some(EditableField {
292            value: EditableFieldString("John".to_string()),
293            label: None,
294            id: None,
295            extensions: None,
296        });
297        let (first, last) = split_name(&full_name);
298        assert_eq!(first, Some("John".to_string()));
299        assert_eq!(last, None);
300    }
301
302    #[test]
303    fn test_split_name_single_name_with_whitespace() {
304        let full_name = Some(EditableField {
305            value: EditableFieldString("  John  ".to_string()),
306            label: None,
307            id: None,
308            extensions: None,
309        });
310        let (first, last) = split_name(&full_name);
311        assert_eq!(first, Some("John".to_string()));
312        assert_eq!(last, None);
313    }
314
315    #[test]
316    fn test_split_name_first_last() {
317        let full_name = Some(EditableField {
318            value: EditableFieldString("John Doe".to_string()),
319            label: None,
320            id: None,
321            extensions: None,
322        });
323        let (first, last) = split_name(&full_name);
324        assert_eq!(first, Some("John".to_string()));
325        assert_eq!(last, Some("Doe".to_string()));
326    }
327
328    #[test]
329    fn test_split_name_first_middle_last() {
330        let full_name = Some(EditableField {
331            value: EditableFieldString("John Michael Doe".to_string()),
332            label: None,
333            id: None,
334            extensions: None,
335        });
336        let (first, last) = split_name(&full_name);
337        assert_eq!(first, Some("John".to_string()));
338        assert_eq!(last, Some("Michael Doe".to_string()));
339    }
340
341    #[test]
342    fn test_split_name_multiple_middle_names() {
343        let full_name = Some(EditableField {
344            value: EditableFieldString("John Michael Andrew Doe".to_string()),
345            label: None,
346            id: None,
347            extensions: None,
348        });
349        let (first, last) = split_name(&full_name);
350        assert_eq!(first, Some("John".to_string()));
351        assert_eq!(last, Some("Michael Andrew Doe".to_string()));
352    }
353
354    #[test]
355    fn test_split_name_complex_surname() {
356        let full_name = Some(EditableField {
357            value: EditableFieldString("Jane van der Berg".to_string()),
358            label: None,
359            id: None,
360            extensions: None,
361        });
362        let (first, last) = split_name(&full_name);
363        assert_eq!(first, Some("Jane".to_string()));
364        assert_eq!(last, Some("van der Berg".to_string()));
365    }
366
367    #[test]
368    fn test_split_name_hyphenated_surname() {
369        let full_name = Some(EditableField {
370            value: EditableFieldString("Mary Smith-Johnson".to_string()),
371            label: None,
372            id: None,
373            extensions: None,
374        });
375        let (first, last) = split_name(&full_name);
376        assert_eq!(first, Some("Mary".to_string()));
377        assert_eq!(last, Some("Smith-Johnson".to_string()));
378    }
379
380    #[test]
381    fn test_split_name_extra_whitespace() {
382        let full_name = Some(EditableField {
383            value: EditableFieldString("  John   Michael   Doe  ".to_string()),
384            label: None,
385            id: None,
386            extensions: None,
387        });
388        let (first, last) = split_name(&full_name);
389        assert_eq!(first, Some("John".to_string()));
390        assert_eq!(last, Some("Michael Doe".to_string()));
391    }
392
393    #[test]
394    fn test_split_name_special_characters() {
395        let full_name = Some(EditableField {
396            value: EditableFieldString("José María González".to_string()),
397            label: None,
398            id: None,
399            extensions: None,
400        });
401        let (first, last) = split_name(&full_name);
402        assert_eq!(first, Some("José".to_string()));
403        assert_eq!(last, Some("María González".to_string()));
404    }
405
406    #[test]
407    fn test_split_name_single_character_names() {
408        let full_name = Some(EditableField {
409            value: EditableFieldString("A B C".to_string()),
410            label: None,
411            id: None,
412            extensions: None,
413        });
414        let (first, last) = split_name(&full_name);
415        assert_eq!(first, Some("A".to_string()));
416        assert_eq!(last, Some("B C".to_string()));
417    }
418}