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 /// Defaults to 0 per RFC 9610 §2.
108 #[serde(default)]
109 pub sort_order: u32,
110 /// True for at most one AddressBook per account; server-set.
111 pub is_default: bool,
112 /// True if the user has subscribed to this AddressBook.
113 pub is_subscribed: bool,
114 /// Map of principal id → rights for principals this AddressBook is shared
115 /// with.
116 ///
117 /// Required-and-nullable (RFC 9610 §2 type: `Id[AddressBookRights]|null`):
118 /// always present in wire JSON; serializes as `null` when not shared.
119 /// The spec §4.1 example shows `"shareWith": null` explicitly — never absent.
120 #[serde(default)]
121 pub share_with: Option<HashMap<Id, AddressBookRights>>,
122 /// ACL rights the authenticated user has on this AddressBook; server-set.
123 pub my_rights: AddressBookRights,
124
125 /// Catch-all for vendor / site / private extension fields not covered
126 /// by the typed fields above. Preserves unknown fields across
127 /// deserialize/serialize round-trip per workspace extras-preservation
128 /// policy (see workspace AGENTS.md).
129 ///
130 /// See [`ContactCard::extra`](crate::ContactCard::extra) for the
131 /// collision contract: programmatically inserting a key that
132 /// matches a typed field name produces a duplicate JSON object key
133 /// on serialize. JMAP-glx8.19.
134 ///
135 /// See [`AddressBook::validate_extras`] for a runtime pre-serialize
136 /// check that detects this hazard. JMAP-glx8.25.
137 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
138 pub extra: serde_json::Map<String, serde_json::Value>,
139}
140
141impl AddressBook {
142 /// Check that no [`extra`](Self::extra) key shadows a typed
143 /// wire-format field of this struct. Returns
144 /// [`Err(CollisionError)`](CollisionError) listing any colliding
145 /// keys; otherwise returns `Ok(())`. See
146 /// [`ContactCard::validate_extras`](crate::ContactCard::validate_extras)
147 /// for the design rationale. JMAP-glx8.25.
148 ///
149 /// # Errors
150 ///
151 /// Returns [`CollisionError`] when one or more keys in
152 /// [`extra`](Self::extra) match one of the camelCase wire-format
153 /// names of this struct's typed fields.
154 pub fn validate_extras(&self) -> Result<(), CollisionError> {
155 collision::check(&self.extra, ADDRESS_BOOK_TYPED_FIELDS)
156 }
157}