Skip to main content

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}