1#![forbid(unsafe_code)]
65
66pub mod addressbook;
67pub mod backend;
68pub mod capability;
69pub mod card;
70
71pub use jmap_jscontact_types as jscontact;
79
80pub use addressbook::{AddressBook, AddressBookRights};
81pub use backend::{AddressBookProperty, ContactCardProperty};
82pub use capability::{ContactsAccountCapability, ContactsCapability, JMAP_CONTACTS_URI};
83pub use card::{ContactCard, ContactCardComparator, ContactCardFilterCondition};
84
85pub use jmap_jscontact_types::{
92 Address, AddressComponent, Anniversary, AnniversaryDate, Author, Calendar, CryptoKey,
93 Directory, EmailAddress, JsContactId, LanguagePref, Link, Media, Name, NameComponent, Nickname,
94 Note, OnlineService, OrgUnit, Organization, PartialDate, PersonalInfo, Phone, Pronouns,
95 Relation, SchedulingAddress, SpeakToAs, Timestamp, Title,
96};
97
98#[cfg(test)]
99mod tests {
100 use std::collections::HashMap;
101
102 use jmap_types::Id;
103 use serde_json::json;
104
105 use super::*;
106
107 #[test]
112 fn address_book_rights_default_is_all_false() {
113 let r = AddressBookRights::default();
114 assert!(!r.may_read, "may_read default should be false");
115 assert!(!r.may_write, "may_write default should be false");
116 assert!(!r.may_share, "may_share default should be false");
117 assert!(!r.may_delete, "may_delete default should be false");
118 }
119
120 #[test]
123 fn address_book_rights_deserialize() {
124 let json = r#"{
125 "mayRead": true,
126 "mayWrite": false,
127 "mayShare": false,
128 "mayDelete": false
129 }"#;
130 let r: AddressBookRights = serde_json::from_str(json).unwrap();
131 assert!(r.may_read);
132 assert!(!r.may_write);
133 assert!(!r.may_share);
134 assert!(!r.may_delete);
135 }
136
137 #[test]
140 fn address_book_rights_round_trip() {
141 let original = AddressBookRights {
142 may_read: true,
143 may_write: true,
144 may_share: false,
145 may_delete: false,
146 extra: serde_json::Map::new(),
147 };
148 let json = serde_json::to_string(&original).unwrap();
149 let decoded: AddressBookRights = serde_json::from_str(&json).unwrap();
150 assert_eq!(original, decoded);
151 }
152
153 #[test]
158 fn address_book_deserialize_spec_example() {
159 let json = r#"{
161 "id": "062adcfa-105d-455c-bc60-6db68b69c3f3",
162 "name": "Personal",
163 "description": null,
164 "sortOrder": 0,
165 "isDefault": true,
166 "isSubscribed": true,
167 "shareWith": {
168 "3f1502e0-63fe-4335-9ff3-e739c188f5dd": {
169 "mayRead": true,
170 "mayWrite": false,
171 "mayShare": false,
172 "mayDelete": false
173 }
174 },
175 "myRights": {
176 "mayRead": true,
177 "mayWrite": true,
178 "mayShare": true,
179 "mayDelete": false
180 }
181 }"#;
182 let ab: AddressBook = serde_json::from_str(json).unwrap();
183 assert_eq!(ab.name, "Personal");
184 assert!(ab.is_default);
185 assert!(ab.is_subscribed);
186 assert_eq!(ab.sort_order, 0);
187 assert!(ab.description.is_none());
188
189 let share_with = ab.share_with.unwrap();
190 let principal_id: Id = Id::from("3f1502e0-63fe-4335-9ff3-e739c188f5dd");
191 let shared_rights = &share_with[&principal_id];
192 assert!(shared_rights.may_read);
193 assert!(!shared_rights.may_write);
194
195 assert!(ab.my_rights.may_read);
196 assert!(ab.my_rights.may_write);
197 assert!(ab.my_rights.may_share);
198 assert!(!ab.my_rights.may_delete);
199 }
200
201 #[test]
204 fn address_book_share_with_null() {
205 let json = r#"{
206 "id": "cd40089d-35f9-4fd7-980b-ba3a9f1d74fe",
207 "name": "Autosaved",
208 "description": null,
209 "sortOrder": 1,
210 "isDefault": false,
211 "isSubscribed": true,
212 "shareWith": null,
213 "myRights": {
214 "mayRead": true,
215 "mayWrite": true,
216 "mayShare": true,
217 "mayDelete": false
218 }
219 }"#;
220 let ab: AddressBook = serde_json::from_str(json).unwrap();
221 assert_eq!(ab.name, "Autosaved");
222 assert!(ab.share_with.is_none());
223 }
224
225 #[test]
229 fn address_book_share_with_null_serializes() {
230 let json = r#"{
232 "id": "ab-null-sw",
233 "name": "Private",
234 "sortOrder": 0,
235 "isDefault": false,
236 "isSubscribed": true,
237 "shareWith": null,
238 "myRights": {
239 "mayRead": true,
240 "mayWrite": true,
241 "mayShare": false,
242 "mayDelete": false
243 }
244 }"#;
245 let ab: AddressBook = serde_json::from_str(json).expect("deserialize");
246 assert!(ab.share_with.is_none());
247
248 let serialized = serde_json::to_value(&ab).expect("serialize");
249 let obj = serialized.as_object().expect("object");
250 assert!(
251 obj.contains_key("shareWith"),
252 "shareWith must be present in wire JSON (required-nullable)"
253 );
254 assert!(
255 obj["shareWith"].is_null(),
256 "shareWith must serialize as null when None"
257 );
258 }
259
260 #[test]
263 fn address_book_share_with_some_serializes() {
264 let json = r#"{
266 "id": "062adcfa-105d-455c-bc60-6db68b69c3f3",
267 "name": "Personal",
268 "description": null,
269 "sortOrder": 0,
270 "isDefault": true,
271 "isSubscribed": true,
272 "shareWith": {
273 "3f1502e0-63fe-4335-9ff3-e739c188f5dd": {
274 "mayRead": true,
275 "mayWrite": false,
276 "mayShare": false,
277 "mayDelete": false
278 }
279 },
280 "myRights": {
281 "mayRead": true,
282 "mayWrite": true,
283 "mayShare": true,
284 "mayDelete": false
285 }
286 }"#;
287 let ab: AddressBook = serde_json::from_str(json).expect("deserialize");
288 assert!(ab.share_with.is_some());
289
290 let serialized = serde_json::to_value(&ab).expect("serialize");
291 let obj = serialized.as_object().expect("object");
292 assert!(
293 obj.contains_key("shareWith"),
294 "shareWith must be present when set"
295 );
296 assert!(
297 !obj["shareWith"].is_null(),
298 "shareWith must not be null when populated"
299 );
300 let back: AddressBook = serde_json::from_value(serialized).expect("round-trip deserialize");
302 assert_eq!(ab.share_with, back.share_with);
303 }
304
305 #[test]
307 fn address_book_round_trip_with_description() {
308 let rights = AddressBookRights {
309 may_read: true,
310 may_write: false,
311 may_share: false,
312 may_delete: false,
313 extra: serde_json::Map::new(),
314 };
315 let original_json = json!({
317 "id": "ab1",
318 "name": "Work",
319 "description": "Work contacts",
320 "sortOrder": 5,
321 "isDefault": false,
322 "isSubscribed": true,
323 "myRights": {
324 "mayRead": true,
325 "mayWrite": false,
326 "mayShare": false,
327 "mayDelete": false
328 }
329 });
330 let ab: AddressBook = serde_json::from_value(original_json).unwrap();
331 assert_eq!(ab.description.as_deref(), Some("Work contacts"));
332 let _rt: AddressBook = serde_json::from_str(&serde_json::to_string(&ab).unwrap()).unwrap();
333 let _ = rights;
335 }
336
337 #[test]
343 fn address_book_description_null_serializes() {
344 let json = r#"{
345 "id": "AB1",
346 "name": "Personal",
347 "description": null,
348 "sortOrder": 0,
349 "isDefault": false,
350 "isSubscribed": false,
351 "shareWith": null,
352 "myRights": {
353 "mayRead": true,
354 "mayWrite": true,
355 "mayShare": false,
356 "mayDelete": false
357 }
358 }"#;
359 let ab: AddressBook = serde_json::from_str(json).expect("deserialize");
360 assert!(ab.description.is_none(), "description should be None");
361
362 let out = serde_json::to_value(&ab).expect("serialize");
363 assert!(
364 out.as_object().expect("object").contains_key("description"),
365 "description key must be present in wire JSON"
366 );
367 assert!(
368 out["description"].is_null(),
369 "description must serialize as null, not be absent"
370 );
371 }
372
373 #[test]
378 fn contact_card_deserialize_spec_example() {
379 let json = r#"{
381 "id": "3",
382 "addressBookIds": {
383 "062adcfa-105d-455c-bc60-6db68b69c3f3": true
384 },
385 "name": {
386 "components": [
387 { "kind": "given", "value": "Joe" },
388 { "kind": "surname", "value": "Bloggs" }
389 ],
390 "isOrdered": true
391 },
392 "emails": {
393 "0": {
394 "contexts": {
395 "private": true
396 },
397 "address": "joe.bloggs@example.com"
398 }
399 }
400 }"#;
401 let card: ContactCard = serde_json::from_str(json).unwrap();
402
403 let id = card.id.as_ref().unwrap();
404 assert_eq!(id.as_ref(), "3");
405
406 let ab_ids = card.address_book_ids.as_ref().unwrap();
407 let ab_key: Id = Id::from("062adcfa-105d-455c-bc60-6db68b69c3f3");
408 assert!(ab_ids[&ab_key]);
409
410 let name = card.name.as_ref().unwrap();
412 assert_eq!(name["isOrdered"], serde_json::Value::Bool(true));
413 assert_eq!(name["components"][0]["kind"], "given");
414 assert_eq!(name["components"][0]["value"], "Joe");
415
416 let emails = card.emails.as_ref().unwrap();
418 assert_eq!(emails["0"]["address"], "joe.bloggs@example.com");
419 }
420
421 #[test]
424 fn contact_card_group_with_members() {
425 let json = r#"{
426 "version": "1.0",
427 "uid": "urn:uuid:ab4310aa-fa43-11e9-8f0b-362b9e155667",
428 "kind": "group",
429 "name": { "full": "The Doe family" },
430 "members": {
431 "urn:uuid:03a0e51f-d1aa-4385-8a53-e29025acd8af": true,
432 "urn:uuid:b8767877-b4a1-4c70-9acc-505d3819e519": true
433 }
434 }"#;
435 let card: ContactCard = serde_json::from_str(json).unwrap();
436 assert_eq!(card.kind.as_deref(), Some("group"));
437 let members = card.members.as_ref().unwrap();
438 assert!(members["urn:uuid:03a0e51f-d1aa-4385-8a53-e29025acd8af"]);
439 assert_eq!(members.len(), 2);
440 }
441
442 #[test]
444 fn contact_card_minimal_fields() {
445 let json = r#"{
446 "id": "minimal1",
447 "addressBookIds": { "ab1": true }
448 }"#;
449 let card: ContactCard = serde_json::from_str(json).unwrap();
450 assert!(card.version.is_none());
451 assert!(card.uid.is_none());
452 assert!(card.kind.is_none());
453 assert!(card.emails.is_none());
454 assert!(card.phones.is_none());
455 assert!(card.addresses.is_none());
456 }
457
458 #[test]
460 fn contact_card_serializes_only_present_fields() {
461 let card: ContactCard = serde_json::from_value(json!({
463 "id": "x1",
464 "addressBookIds": { "ab1": true },
465 "version": "1.0",
466 "uid": "urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6"
467 }))
468 .unwrap();
469
470 let serialized = serde_json::to_value(&card).unwrap();
471 assert!(serialized.get("id").is_some());
473 assert!(serialized.get("version").is_some());
474 assert!(serialized.get("uid").is_some());
475 assert!(serialized.get("emails").is_none());
477 assert!(serialized.get("phones").is_none());
478 assert!(serialized.get("addresses").is_none());
479 assert!(serialized.get("kind").is_none());
480 }
481
482 #[test]
489 fn contact_card_localizations_patch_object_transparent() {
490 let json = r#"{"localizations":{"de":{"name/full":"Joe Bloggs (DE)"}}}"#;
491 let card: ContactCard = serde_json::from_str(json).expect("deserialize");
492
493 let locs = card.localizations.as_ref().expect("localizations present");
494 let de = locs.get("de").expect("de locale present");
495 assert_eq!(
496 de.as_map().get("name/full").and_then(|v| v.as_str()),
497 Some("Joe Bloggs (DE)"),
498 );
499
500 let serialized = serde_json::to_string(&card).expect("serialize");
501 assert_eq!(serialized, json);
502 }
503
504 #[test]
510 fn filter_condition_slash_keys_serialize_correctly() {
511 let filter: ContactCardFilterCondition = serde_json::from_value(json!({
512 "name/given": "John",
513 "name/surname": "Smith",
514 "name/surname2": "Doe"
515 }))
516 .unwrap();
517
518 assert_eq!(filter.name_given.as_deref(), Some("John"));
519 assert_eq!(filter.name_surname.as_deref(), Some("Smith"));
520 assert_eq!(filter.name_surname2.as_deref(), Some("Doe"));
521
522 let serialized = serde_json::to_value(&filter).unwrap();
523 assert_eq!(serialized["name/given"], "John");
524 assert_eq!(serialized["name/surname"], "Smith");
525 assert_eq!(serialized["name/surname2"], "Doe");
526 }
527
528 #[test]
531 fn filter_condition_all_fields() {
532 let json = r#"{
533 "inAddressBook": "ab1",
534 "uid": "urn:uuid:abc123",
535 "hasMember": "urn:uuid:def456",
536 "kind": "individual",
537 "createdBefore": "2024-01-01T00:00:00Z",
538 "createdAfter": "2020-01-01T00:00:00Z",
539 "updatedBefore": "2024-06-01T00:00:00Z",
540 "updatedAfter": "2021-06-01T00:00:00Z",
541 "text": "John",
542 "name": "Doe",
543 "name/given": "Jane",
544 "name/surname": "Smith",
545 "name/surname2": "Gomez",
546 "nickname": "Johnny",
547 "organization": "ACME",
548 "email": "john@example.com",
549 "phone": "+1-555-1234",
550 "onlineService": "twitter",
551 "address": "Main St",
552 "note": "VIP"
553 }"#;
554 let f: ContactCardFilterCondition = serde_json::from_str(json).unwrap();
555 assert_eq!(
556 f.in_address_book.as_ref().map(|id| id.as_ref()),
557 Some("ab1")
558 );
559 assert_eq!(f.uid.as_deref(), Some("urn:uuid:abc123"));
560 assert_eq!(f.has_member.as_deref(), Some("urn:uuid:def456"));
561 assert_eq!(f.kind.as_deref(), Some("individual"));
562 assert_eq!(
563 f.created_before.as_ref().map(AsRef::as_ref),
564 Some("2024-01-01T00:00:00Z")
565 );
566 assert_eq!(
567 f.created_after.as_ref().map(AsRef::as_ref),
568 Some("2020-01-01T00:00:00Z")
569 );
570 assert_eq!(
571 f.updated_before.as_ref().map(AsRef::as_ref),
572 Some("2024-06-01T00:00:00Z")
573 );
574 assert_eq!(
575 f.updated_after.as_ref().map(AsRef::as_ref),
576 Some("2021-06-01T00:00:00Z")
577 );
578 assert_eq!(f.text.as_deref(), Some("John"));
579 assert_eq!(f.name.as_deref(), Some("Doe"));
580 assert_eq!(f.name_given.as_deref(), Some("Jane"));
581 assert_eq!(f.name_surname.as_deref(), Some("Smith"));
582 assert_eq!(f.name_surname2.as_deref(), Some("Gomez"));
583 assert_eq!(f.nickname.as_deref(), Some("Johnny"));
584 assert_eq!(f.organization.as_deref(), Some("ACME"));
585 assert_eq!(f.email.as_deref(), Some("john@example.com"));
586 assert_eq!(f.phone.as_deref(), Some("+1-555-1234"));
587 assert_eq!(f.online_service.as_deref(), Some("twitter"));
588 assert_eq!(f.address.as_deref(), Some("Main St"));
589 assert_eq!(f.note.as_deref(), Some("VIP"));
590 }
591
592 #[test]
594 fn filter_condition_empty_serializes_to_empty_object() {
595 let f = ContactCardFilterCondition::default();
596 let v = serde_json::to_value(&f).unwrap();
597 assert_eq!(v, json!({}));
598 }
599
600 #[test]
605 fn contacts_capability_is_empty_object() {
606 let cap = ContactsCapability::default();
607 let v = serde_json::to_value(&cap).unwrap();
608 assert_eq!(v, json!({}));
609 }
610
611 #[test]
614 fn contacts_account_capability_deserialize() {
615 let json = r#"{
616 "maxAddressBooksPerCard": 5,
617 "mayCreateAddressBook": true
618 }"#;
619 let cap: ContactsAccountCapability = serde_json::from_str(json).unwrap();
620 assert_eq!(cap.max_address_books_per_card, Some(5));
621 assert!(cap.may_create_address_book);
622 }
623
624 #[test]
627 fn contacts_account_capability_null_max() {
628 let json = r#"{
629 "maxAddressBooksPerCard": null,
630 "mayCreateAddressBook": false
631 }"#;
632 let cap: ContactsAccountCapability = serde_json::from_str(json).unwrap();
633 assert!(cap.max_address_books_per_card.is_none());
634 assert!(!cap.may_create_address_book);
635 }
636
637 #[test]
639 fn jmap_contacts_uri_value() {
640 assert_eq!(JMAP_CONTACTS_URI, "urn:ietf:params:jmap:contacts");
641 }
642
643 #[test]
648 fn comparator_is_ascending_defaults_to_true() {
649 let json = r#"{ "property": "created" }"#;
650 let c: ContactCardComparator = serde_json::from_str(json).unwrap();
651 assert!(c.is_ascending);
652 assert!(c.collation.is_none());
653 }
654
655 #[test]
657 fn comparator_round_trip() {
658 let json = r#"{ "property": "name/given", "isAscending": false, "collation": "i;unicode-casemap" }"#;
659 let c: ContactCardComparator = serde_json::from_str(json).unwrap();
660 assert_eq!(c.property, "name/given");
661 assert!(!c.is_ascending);
662 assert_eq!(c.collation.as_deref(), Some("i;unicode-casemap"));
663 let rt: ContactCardComparator =
664 serde_json::from_str(&serde_json::to_string(&c).unwrap()).unwrap();
665 assert_eq!(rt.property, "name/given");
666 assert!(!rt.is_ascending);
667 }
668
669 fn _use_imports() {
671 let _: HashMap<String, bool> = HashMap::new();
672 }
673
674 #[test]
689 fn sloppy_field_name_roundtrips_through_jscontact_name() {
690 let card_json = json!({
691 "name": {
692 "components": [
693 { "kind": "given", "value": "Vincent" },
694 { "kind": "surname", "value": "van Gogh" }
695 ],
696 "isOrdered": true
697 }
698 });
699 let card: ContactCard = serde_json::from_value(card_json).unwrap();
700 let name: Name = serde_json::from_value(card.name.clone().unwrap()).unwrap();
701 let back = serde_json::to_value(&name).unwrap();
702 assert_eq!(back, card.name.unwrap());
703 }
704
705 #[test]
708 fn sloppy_field_nicknames_roundtrips_through_jscontact_nickname_map() {
709 let card_json = json!({
710 "nicknames": {
711 "k391": { "name": "Johnny" }
712 }
713 });
714 let card: ContactCard = serde_json::from_value(card_json).unwrap();
715 let nicks: HashMap<String, Nickname> =
716 serde_json::from_value(card.nicknames.clone().unwrap()).unwrap();
717 assert_eq!(nicks["k391"].name, "Johnny");
718 let back = serde_json::to_value(&nicks).unwrap();
719 assert_eq!(back, card.nicknames.unwrap());
720 }
721
722 #[test]
725 fn sloppy_field_organizations_roundtrips_through_jscontact_organization_map() {
726 let card_json = json!({
727 "organizations": {
728 "o1": {
729 "name": "ABC, Inc.",
730 "units": [
731 { "name": "North American Division" },
732 { "name": "Marketing" }
733 ],
734 "sortAs": "ABC"
735 }
736 }
737 });
738 let card: ContactCard = serde_json::from_value(card_json).unwrap();
739 let orgs: HashMap<String, Organization> =
740 serde_json::from_value(card.organizations.clone().unwrap()).unwrap();
741 assert_eq!(orgs["o1"].name.as_deref(), Some("ABC, Inc."));
742 let back = serde_json::to_value(&orgs).unwrap();
743 assert_eq!(back, card.organizations.unwrap());
744 }
745
746 #[test]
749 fn sloppy_field_speak_to_as_roundtrips_through_jscontact_speak_to_as() {
750 let card_json = json!({
751 "speakToAs": {
752 "grammaticalGender": "neuter",
753 "pronouns": {
754 "k19": { "pronouns": "they/them", "pref": 2 }
755 }
756 }
757 });
758 let card: ContactCard = serde_json::from_value(card_json).unwrap();
759 let speak: SpeakToAs = serde_json::from_value(card.speak_to_as.clone().unwrap()).unwrap();
760 let back = serde_json::to_value(&speak).unwrap();
761 assert_eq!(back, card.speak_to_as.unwrap());
762 }
763
764 #[test]
767 fn sloppy_field_titles_roundtrips_through_jscontact_title_map() {
768 let card_json = json!({
769 "titles": {
770 "le9": { "kind": "title", "name": "Research Scientist" }
771 }
772 });
773 let card: ContactCard = serde_json::from_value(card_json).unwrap();
774 let titles: HashMap<String, Title> =
775 serde_json::from_value(card.titles.clone().unwrap()).unwrap();
776 assert_eq!(titles["le9"].name, "Research Scientist");
777 let back = serde_json::to_value(&titles).unwrap();
778 assert_eq!(back, card.titles.unwrap());
779 }
780
781 #[test]
784 fn sloppy_field_emails_roundtrips_through_jscontact_email_address_map() {
785 let card_json = json!({
786 "emails": {
787 "e1": { "contexts": { "work": true }, "address": "jqpublic@xyz.example.com" }
788 }
789 });
790 let card: ContactCard = serde_json::from_value(card_json).unwrap();
791 let emails: HashMap<String, EmailAddress> =
792 serde_json::from_value(card.emails.clone().unwrap()).unwrap();
793 assert_eq!(emails["e1"].address, "jqpublic@xyz.example.com");
794 let back = serde_json::to_value(&emails).unwrap();
795 assert_eq!(back, card.emails.unwrap());
796 }
797
798 #[test]
801 fn sloppy_field_online_services_roundtrips_through_jscontact_online_service_map() {
802 let card_json = json!({
803 "onlineServices": {
804 "x1": { "uri": "xmpp:alice@example.com" }
805 }
806 });
807 let card: ContactCard = serde_json::from_value(card_json).unwrap();
808 let svcs: HashMap<String, OnlineService> =
809 serde_json::from_value(card.online_services.clone().unwrap()).unwrap();
810 let back = serde_json::to_value(&svcs).unwrap();
811 assert_eq!(back, card.online_services.unwrap());
812 }
813
814 #[test]
817 fn sloppy_field_phones_roundtrips_through_jscontact_phone_map() {
818 let card_json = json!({
819 "phones": {
820 "tel0": {
821 "contexts": { "private": true },
822 "features": { "voice": true },
823 "number": "tel:+1-555-555-5555;ext=5555",
824 "pref": 1
825 }
826 }
827 });
828 let card: ContactCard = serde_json::from_value(card_json).unwrap();
829 let phones: HashMap<String, Phone> =
830 serde_json::from_value(card.phones.clone().unwrap()).unwrap();
831 assert_eq!(phones["tel0"].number, "tel:+1-555-555-5555;ext=5555");
832 let back = serde_json::to_value(&phones).unwrap();
833 assert_eq!(back, card.phones.unwrap());
834 }
835
836 #[test]
839 fn sloppy_field_preferred_languages_roundtrips_through_jscontact_language_pref_map() {
840 let card_json = json!({
841 "preferredLanguages": {
842 "l1": { "language": "en", "contexts": { "work": true }, "pref": 1 }
843 }
844 });
845 let card: ContactCard = serde_json::from_value(card_json).unwrap();
846 let langs: HashMap<String, LanguagePref> =
847 serde_json::from_value(card.preferred_languages.clone().unwrap()).unwrap();
848 assert_eq!(langs["l1"].language, "en");
849 let back = serde_json::to_value(&langs).unwrap();
850 assert_eq!(back, card.preferred_languages.unwrap());
851 }
852
853 #[test]
856 fn sloppy_field_calendars_roundtrips_through_jscontact_calendar_map() {
857 let card_json = json!({
858 "calendars": {
859 "calA": { "kind": "calendar", "uri": "webcal://calendar.example.com/calA.ics" }
860 }
861 });
862 let card: ContactCard = serde_json::from_value(card_json).unwrap();
863 let cals: HashMap<String, Calendar> =
864 serde_json::from_value(card.calendars.clone().unwrap()).unwrap();
865 let back = serde_json::to_value(&cals).unwrap();
866 assert_eq!(back, card.calendars.unwrap());
867 }
868
869 #[test]
872 fn sloppy_field_scheduling_addresses_roundtrips_through_jscontact_scheduling_address_map() {
873 let card_json = json!({
874 "schedulingAddresses": {
875 "sched1": { "uri": "mailto:janedoe@example.com" }
876 }
877 });
878 let card: ContactCard = serde_json::from_value(card_json).unwrap();
879 let scheds: HashMap<String, SchedulingAddress> =
880 serde_json::from_value(card.scheduling_addresses.clone().unwrap()).unwrap();
881 assert_eq!(scheds["sched1"].uri, "mailto:janedoe@example.com");
882 let back = serde_json::to_value(&scheds).unwrap();
883 assert_eq!(back, card.scheduling_addresses.unwrap());
884 }
885
886 #[test]
889 fn sloppy_field_addresses_roundtrips_through_jscontact_address_map() {
890 let card_json = json!({
891 "addresses": {
892 "k23": {
893 "contexts": { "work": true },
894 "components": [
895 { "kind": "number", "value": "54321" },
896 { "kind": "name", "value": "Oak St" }
897 ],
898 "countryCode": "US",
899 "isOrdered": true
900 }
901 }
902 });
903 let card: ContactCard = serde_json::from_value(card_json).unwrap();
904 let addrs: HashMap<String, Address> =
905 serde_json::from_value(card.addresses.clone().unwrap()).unwrap();
906 assert_eq!(addrs["k23"].country_code.as_deref(), Some("US"));
907 let back = serde_json::to_value(&addrs).unwrap();
908 assert_eq!(back, card.addresses.unwrap());
909 }
910
911 #[test]
914 fn sloppy_field_crypto_keys_roundtrips_through_jscontact_crypto_key_map() {
915 let card_json = json!({
916 "cryptoKeys": {
917 "mykey1": { "uri": "https://www.example.com/keys/jdoe.cer" }
918 }
919 });
920 let card: ContactCard = serde_json::from_value(card_json).unwrap();
921 let keys: HashMap<String, CryptoKey> =
922 serde_json::from_value(card.crypto_keys.clone().unwrap()).unwrap();
923 let back = serde_json::to_value(&keys).unwrap();
924 assert_eq!(back, card.crypto_keys.unwrap());
925 }
926
927 #[test]
930 fn sloppy_field_directories_roundtrips_through_jscontact_directory_map() {
931 let card_json = json!({
932 "directories": {
933 "dir1": {
934 "kind": "entry",
935 "uri": "https://dir.example.com/addrbook/jdoe/Jean%20Dupont.vcf"
936 }
937 }
938 });
939 let card: ContactCard = serde_json::from_value(card_json).unwrap();
940 let dirs: HashMap<String, Directory> =
941 serde_json::from_value(card.directories.clone().unwrap()).unwrap();
942 assert_eq!(dirs["dir1"].kind.as_deref(), Some("entry"));
943 let back = serde_json::to_value(&dirs).unwrap();
944 assert_eq!(back, card.directories.unwrap());
945 }
946
947 #[test]
950 fn sloppy_field_links_roundtrips_through_jscontact_link_map() {
951 let card_json = json!({
952 "links": {
953 "link3": { "kind": "contact", "uri": "mailto:contact@example.com", "pref": 1 }
954 }
955 });
956 let card: ContactCard = serde_json::from_value(card_json).unwrap();
957 let links: HashMap<String, Link> =
958 serde_json::from_value(card.links.clone().unwrap()).unwrap();
959 let back = serde_json::to_value(&links).unwrap();
960 assert_eq!(back, card.links.unwrap());
961 }
962
963 #[test]
966 fn sloppy_field_media_roundtrips_through_jscontact_media_map() {
967 let card_json = json!({
968 "media": {
969 "res47": { "kind": "logo", "uri": "https://www.example.com/pub/logos/abccorp.jpg" }
970 }
971 });
972 let card: ContactCard = serde_json::from_value(card_json).unwrap();
973 let media: HashMap<String, Media> =
974 serde_json::from_value(card.media.clone().unwrap()).unwrap();
975 assert_eq!(media["res47"].kind.as_deref(), Some("logo"));
976 let back = serde_json::to_value(&media).unwrap();
977 assert_eq!(back, card.media.unwrap());
978 }
979
980 #[test]
983 fn sloppy_field_anniversaries_roundtrips_through_jscontact_anniversary_map() {
984 let card_json = json!({
985 "anniversaries": {
986 "k8": {
987 "kind": "birth",
988 "date": { "year": 1953, "month": 4, "day": 15 }
989 }
990 }
991 });
992 let card: ContactCard = serde_json::from_value(card_json).unwrap();
993 let annivs: HashMap<String, Anniversary> =
994 serde_json::from_value(card.anniversaries.clone().unwrap()).unwrap();
995 assert_eq!(annivs["k8"].kind, "birth");
996 let back = serde_json::to_value(&annivs).unwrap();
997 assert_eq!(back, card.anniversaries.unwrap());
998 }
999
1000 #[test]
1003 fn sloppy_field_notes_roundtrips_through_jscontact_note_map() {
1004 let card_json = json!({
1005 "notes": {
1006 "n1": {
1007 "note": "Open office hours are 1600 to 1715 EST, Mon-Fri",
1008 "created": "2022-11-23T15:01:32Z",
1009 "author": { "name": "John" }
1010 }
1011 }
1012 });
1013 let card: ContactCard = serde_json::from_value(card_json).unwrap();
1014 let notes: HashMap<String, Note> =
1015 serde_json::from_value(card.notes.clone().unwrap()).unwrap();
1016 assert_eq!(
1017 notes["n1"].author.as_ref().unwrap().name.as_deref(),
1018 Some("John")
1019 );
1020 let back = serde_json::to_value(¬es).unwrap();
1021 assert_eq!(back, card.notes.unwrap());
1022 }
1023
1024 #[test]
1027 fn sloppy_field_personal_info_roundtrips_through_jscontact_personal_info_map() {
1028 let card_json = json!({
1029 "personalInfo": {
1030 "pi2": { "kind": "expertise", "value": "chemistry", "level": "high" }
1031 }
1032 });
1033 let card: ContactCard = serde_json::from_value(card_json).unwrap();
1034 let info: HashMap<String, PersonalInfo> =
1035 serde_json::from_value(card.personal_info.clone().unwrap()).unwrap();
1036 assert_eq!(info["pi2"].kind, "expertise");
1037 let back = serde_json::to_value(&info).unwrap();
1038 assert_eq!(back, card.personal_info.unwrap());
1039 }
1040
1041 #[test]
1044 fn sloppy_field_related_to_roundtrips_through_jscontact_relation_map() {
1045 let card_json = json!({
1046 "relatedTo": {
1047 "urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6": {
1048 "relation": { "friend": true }
1049 }
1050 }
1051 });
1052 let card: ContactCard = serde_json::from_value(card_json).unwrap();
1053 let rels: HashMap<String, Relation> =
1054 serde_json::from_value(card.related_to.clone().unwrap()).unwrap();
1055 let back = serde_json::to_value(&rels).unwrap();
1056 assert_eq!(back, card.related_to.unwrap());
1057 }
1058
1059 #[test]
1066 fn sloppy_field_keywords_roundtrips_through_string_bool_map() {
1067 let card_json = json!({
1068 "keywords": {
1069 "internet": true,
1070 "IETF": true
1071 }
1072 });
1073 let card: ContactCard = serde_json::from_value(card_json).unwrap();
1074 let kws: HashMap<String, bool> =
1075 serde_json::from_value(card.keywords.clone().unwrap()).unwrap();
1076 assert!(kws["internet"]);
1077 let back = serde_json::to_value(&kws).unwrap();
1078 assert_eq!(back, card.keywords.unwrap());
1079 }
1080
1081 #[test]
1090 fn jscontact_module_alias_is_jmap_jscontact_types() {
1091 let v = json!({ "full": "Vincent van Gogh" });
1092 let direct: jmap_jscontact_types::Name = serde_json::from_value(v.clone()).unwrap();
1093 let aliased: crate::jscontact::Name = serde_json::from_value(v).unwrap();
1094 assert_eq!(direct, aliased);
1096 let top_level: Name = direct.clone();
1098 assert_eq!(direct, top_level);
1099 }
1100
1101 #[test]
1105 fn address_book_rights_preserves_vendor_extras() {
1106 let raw = json!({
1107 "mayRead": true,
1108 "mayWrite": false,
1109 "mayShare": false,
1110 "mayDelete": false,
1111 "acmeCorpMayMerge": true
1112 });
1113 let r: AddressBookRights = serde_json::from_value(raw).unwrap();
1114 assert_eq!(
1115 r.extra.get("acmeCorpMayMerge").and_then(|v| v.as_bool()),
1116 Some(true)
1117 );
1118 let back = serde_json::to_value(&r).unwrap();
1119 assert_eq!(back["acmeCorpMayMerge"], true);
1120 }
1121
1122 #[test]
1124 fn address_book_preserves_vendor_extras() {
1125 let raw = json!({
1126 "id": "ab1",
1127 "name": "Personal",
1128 "description": null,
1129 "sortOrder": 0,
1130 "isDefault": true,
1131 "isSubscribed": true,
1132 "shareWith": null,
1133 "myRights": {
1134 "mayRead": true, "mayWrite": true,
1135 "mayShare": false, "mayDelete": false
1136 },
1137 "acmeCorpRetentionDays": 365
1138 });
1139 let ab: AddressBook = serde_json::from_value(raw).unwrap();
1140 assert_eq!(
1141 ab.extra
1142 .get("acmeCorpRetentionDays")
1143 .and_then(|v| v.as_u64()),
1144 Some(365)
1145 );
1146 let back = serde_json::to_value(&ab).unwrap();
1147 assert_eq!(back["acmeCorpRetentionDays"], 365);
1148 }
1149
1150 #[test]
1152 fn contact_card_preserves_vendor_extras() {
1153 let raw = json!({
1154 "uid": "card-1",
1155 "version": "1.0",
1156 "acmeCorpExternalId": "ldap-42"
1157 });
1158 let c: ContactCard = serde_json::from_value(raw).unwrap();
1159 assert_eq!(
1160 c.extra.get("acmeCorpExternalId").and_then(|v| v.as_str()),
1161 Some("ldap-42")
1162 );
1163 let back = serde_json::to_value(&c).unwrap();
1164 assert_eq!(back["acmeCorpExternalId"], "ldap-42");
1165 }
1166}