jmap_contacts_types/addressbook.rs
1//! RFC 9610 §2 — AddressBook object and component types.
2//!
3//! Provides [`AddressBook`] and [`AddressBookRights`].
4//!
5//! Note: the spec defines no `AddressBook/query` method, so no
6//! `AddressBookFilterCondition` type exists.
7
8use std::collections::HashMap;
9
10use jmap_types::Id;
11use serde::{Deserialize, Serialize};
12
13use crate::collision::{self, CollisionError};
14
15/// Camel-case wire-format names of every typed field on
16/// [`AddressBookRights`]. Used by
17/// [`AddressBookRights::validate_extras`]. JMAP-glx8.25.
18const ADDRESS_BOOK_RIGHTS_TYPED_FIELDS: &[&str] = &["mayRead", "mayWrite", "mayShare", "mayDelete"];
19
20/// Camel-case wire-format names of every typed field on [`AddressBook`].
21/// Used by [`AddressBook::validate_extras`]. JMAP-glx8.25.
22const ADDRESS_BOOK_TYPED_FIELDS: &[&str] = &[
23 "id",
24 "name",
25 "description",
26 "sortOrder",
27 "isDefault",
28 "isSubscribed",
29 "shareWith",
30 "myRights",
31];
32
33/// Access rights a principal holds on an AddressBook (RFC 9610 §2).
34///
35/// All four rights are booleans. `Default` produces all-false, which is the
36/// most restrictive valid value and a safe starting point when constructing
37/// rights in tests or server code.
38#[non_exhaustive]
39#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
40#[serde(rename_all = "camelCase")]
41pub struct AddressBookRights {
42 /// User may fetch the ContactCards in this AddressBook.
43 pub may_read: bool,
44 /// User may create, modify, or destroy all ContactCards in this AddressBook,
45 /// or move them to or from this AddressBook.
46 pub may_write: bool,
47 /// User may modify the `shareWith` property for this AddressBook.
48 pub may_share: bool,
49 /// User may delete the AddressBook itself.
50 pub may_delete: bool,
51
52 /// Catch-all for vendor / site / private extension fields not covered
53 /// by the typed fields above. Preserves unknown fields across
54 /// deserialize/serialize round-trip per workspace extras-preservation
55 /// policy (see workspace AGENTS.md).
56 ///
57 /// See [`ContactCard::extra`](crate::ContactCard::extra) for the
58 /// collision contract: programmatically inserting a key that
59 /// matches a typed field name produces a duplicate JSON object key
60 /// on serialize. Treat `extra` as a write-only catch-all for unknown
61 /// keys discovered at deserialize. JMAP-glx8.19.
62 ///
63 /// See [`AddressBookRights::validate_extras`] for a runtime
64 /// pre-serialize check that detects this hazard. JMAP-glx8.25.
65 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
66 pub extra: serde_json::Map<String, serde_json::Value>,
67}
68
69impl AddressBookRights {
70 /// Check that no [`extra`](Self::extra) key shadows a typed
71 /// wire-format field of this struct. Returns
72 /// [`Err(CollisionError)`](CollisionError) listing any colliding
73 /// keys; otherwise returns `Ok(())`. See
74 /// [`ContactCard::validate_extras`](crate::ContactCard::validate_extras)
75 /// for the design rationale. JMAP-glx8.25.
76 ///
77 /// # Errors
78 ///
79 /// Returns [`CollisionError`] when one or more keys in
80 /// [`extra`](Self::extra) match one of the camelCase wire-format
81 /// names of this struct's typed fields.
82 pub fn validate_extras(&self) -> Result<(), CollisionError> {
83 collision::check(&self.extra, ADDRESS_BOOK_RIGHTS_TYPED_FIELDS)
84 }
85}
86
87/// A JMAP AddressBook object (RFC 9610 §2).
88///
89/// An AddressBook is a named collection of ContactCards. All ContactCards
90/// belong to one or more AddressBooks.
91#[non_exhaustive]
92#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
93#[serde(rename_all = "camelCase")]
94pub struct AddressBook {
95 /// Server-assigned immutable identifier.
96 pub id: Id,
97 /// User-visible name; MUST NOT be empty and MUST NOT exceed 255 UTF-8 octets.
98 pub name: String,
99 /// Optional longer-form description (default: null).
100 ///
101 /// Required-and-nullable per RFC 9553 + RFC 9610 §4.1: always present on
102 /// the wire as `"description": null` when unset. Must NOT use
103 /// `skip_serializing_if`.
104 #[serde(default)]
105 pub description: Option<String>,
106 /// UI sort order; lower values display first. Range: [0, 2^31).
107 pub sort_order: u32,
108 /// True for at most one AddressBook per account; server-set.
109 pub is_default: bool,
110 /// True if the user has subscribed to this AddressBook.
111 pub is_subscribed: bool,
112 /// Map of principal id → rights for principals this AddressBook is shared
113 /// with.
114 ///
115 /// Required-and-nullable (RFC 9610 §2 type: `Id[AddressBookRights]|null`):
116 /// always present in wire JSON; serializes as `null` when not shared.
117 /// The spec §4.1 example shows `"shareWith": null` explicitly — never absent.
118 #[serde(default)]
119 pub share_with: Option<HashMap<Id, AddressBookRights>>,
120 /// ACL rights the authenticated user has on this AddressBook; server-set.
121 pub my_rights: AddressBookRights,
122
123 /// Catch-all for vendor / site / private extension fields not covered
124 /// by the typed fields above. Preserves unknown fields across
125 /// deserialize/serialize round-trip per workspace extras-preservation
126 /// policy (see workspace AGENTS.md).
127 ///
128 /// See [`ContactCard::extra`](crate::ContactCard::extra) for the
129 /// collision contract: programmatically inserting a key that
130 /// matches a typed field name produces a duplicate JSON object key
131 /// on serialize. JMAP-glx8.19.
132 ///
133 /// See [`AddressBook::validate_extras`] for a runtime pre-serialize
134 /// check that detects this hazard. JMAP-glx8.25.
135 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
136 pub extra: serde_json::Map<String, serde_json::Value>,
137}
138
139impl AddressBook {
140 /// Check that no [`extra`](Self::extra) key shadows a typed
141 /// wire-format field of this struct. Returns
142 /// [`Err(CollisionError)`](CollisionError) listing any colliding
143 /// keys; otherwise returns `Ok(())`. See
144 /// [`ContactCard::validate_extras`](crate::ContactCard::validate_extras)
145 /// for the design rationale. JMAP-glx8.25.
146 ///
147 /// # Errors
148 ///
149 /// Returns [`CollisionError`] when one or more keys in
150 /// [`extra`](Self::extra) match one of the camelCase wire-format
151 /// names of this struct's typed fields.
152 pub fn validate_extras(&self) -> Result<(), CollisionError> {
153 collision::check(&self.extra, ADDRESS_BOOK_TYPED_FIELDS)
154 }
155}