io_jmap/rfc9610/address_book/set.rs
1//! JMAP `AddressBook/set` coroutine (RFC 9610 §2.3): wraps the generic
2//! [`JmapSet`] with [`JmapAddressBookSetArgs`] (create/update/destroy plus
3//! the `onDestroyRemoveContents` and `onSuccessSetIsDefault` extra
4//! arguments) and decodes per-object [`JmapAddressBookSetItemError`]
5//! payloads.
6//!
7//! # Example
8//!
9//! ```rust,no_run
10//! use std::{
11//! io::{Read, Write},
12//! net::TcpStream,
13//! };
14//!
15//! use io_jmap::{
16//! coroutine::{JmapCoroutine, JmapCoroutineState, JmapYield},
17//! rfc8620::session::JmapSession,
18//! rfc9610::address_book::set::{JmapAddressBookSet, JmapAddressBookSetArgs},
19//! };
20//! use secrecy::SecretString;
21//!
22//! // Ready stream needed (TCP-connected, TLS-negociated)
23//! let mut stream = TcpStream::connect("api.example.com:443").unwrap();
24//! let mut buf = [0u8; 4096];
25//!
26//! let session: JmapSession = serde_json::from_str(r#"{
27//! "username": "",
28//! "accounts": {},
29//! "primaryAccounts": {"urn:ietf:params:jmap:contacts": "a1"},
30//! "capabilities": {},
31//! "apiUrl": "https://api.example.com/jmap/",
32//! "downloadUrl": "",
33//! "uploadUrl": "",
34//! "eventSourceUrl": "",
35//! "state": ""
36//! }"#).unwrap();
37//! let auth = SecretString::from("Bearer xyz");
38//! let mut coroutine =
39//! JmapAddressBookSet::new(&session, &auth, JmapAddressBookSetArgs::default()).unwrap();
40//! let mut arg = None;
41//!
42//! let out = loop {
43//! match coroutine.resume(arg.take()) {
44//! JmapCoroutineState::Yielded(JmapYield::WantsWrite(bytes)) => {
45//! stream.write_all(&bytes).unwrap();
46//! }
47//! JmapCoroutineState::Yielded(JmapYield::WantsRead) => {
48//! let n = stream.read(&mut buf).unwrap();
49//! arg = Some(&buf[..n]);
50//! }
51//! JmapCoroutineState::Complete(Ok(out)) => break out,
52//! JmapCoroutineState::Complete(Err(err)) => panic!("{err}"),
53//! }
54//! };
55//!
56//! println!("new state {}", out.new_state);
57//! ```
58
59use alloc::{collections::BTreeMap, string::String, vec, vec::Vec};
60
61use secrecy::SecretString;
62use serde::{Deserialize, Serialize};
63use thiserror::Error;
64
65use crate::{
66 coroutine::*,
67 jmap_try,
68 rfc8620::{JMAP_CORE_CAPABILITY, request::JmapBatch, send::*, session::JmapSession, set::*},
69 rfc9610::{
70 JMAP_CONTACTS_CAPABILITY,
71 address_book::{JmapAddressBook, JmapAddressBookRights},
72 },
73};
74
75/// Client-settable subset of [`JmapAddressBook`] for `AddressBook/set`
76/// create requests (RFC 9610 §2). Server-assigned fields are excluded.
77#[derive(Clone, Debug, Default, Serialize)]
78#[serde(rename_all = "camelCase")]
79pub struct JmapAddressBookCreate {
80 /// The user-visible address book name.
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub name: Option<String>,
83 /// Optional long-form description.
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub description: Option<String>,
86 /// Position hint for display ordering (lower first).
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub sort_order: Option<u32>,
89 /// Whether the user is subscribed to the address book.
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub is_subscribed: Option<bool>,
92 /// Principal id to rights map (RFC 9670).
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub share_with: Option<BTreeMap<String, JmapAddressBookRights>>,
95}
96
97/// Patch object for `AddressBook/set` update requests (RFC 8620 §5.3): only
98/// `Some` fields are serialised.
99#[derive(Clone, Debug, Default, Serialize)]
100#[serde(rename_all = "camelCase")]
101pub struct JmapAddressBookUpdate {
102 /// The user-visible address book name.
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub name: Option<String>,
105 /// Optional long-form description.
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub description: Option<String>,
108 /// Position hint for display ordering (lower first).
109 #[serde(skip_serializing_if = "Option::is_none")]
110 pub sort_order: Option<u32>,
111 /// Whether the user is subscribed to the address book.
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub is_subscribed: Option<bool>,
114 /// Principal id to rights map (RFC 9670).
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub share_with: Option<BTreeMap<String, JmapAddressBookRights>>,
117}
118
119/// Per-object error returned in `AddressBook/set` responses (RFC 9610 §2.3).
120///
121/// Covers the standard RFC 8620 §5.3 set errors plus the AddressBook-specific
122/// error defined in RFC 9610 §2.3.
123#[derive(Clone, Debug, Deserialize)]
124#[serde(tag = "type", rename_all = "camelCase")]
125pub enum JmapAddressBookSetItemError {
126 /// The AddressBook still has ContactCards and `onDestroyRemoveContents`
127 /// was false (RFC 9610 §2.3).
128 AddressBookHasContents {
129 /// Optional human-readable detail.
130 description: Option<String>,
131 },
132 /// Standard set error (RFC 8620 §5.3): the change is not allowed, e.g.
133 /// a `shareWith` or `isSubscribed` change rejected by the server.
134 Forbidden {
135 /// Optional human-readable detail.
136 description: Option<String>,
137 },
138 /// Standard set error (RFC 8620 §5.3): target id not found.
139 NotFound {
140 /// Optional human-readable detail.
141 description: Option<String>,
142 },
143 /// Standard set error (RFC 8620 §5.3): patch could not be applied.
144 InvalidPatch {
145 /// Optional human-readable detail.
146 description: Option<String>,
147 },
148 /// Standard set error (RFC 8620 §5.3): would destroy an object already
149 /// queued for destruction in the same request.
150 WillDestroy {
151 /// Optional human-readable detail.
152 description: Option<String>,
153 },
154 /// Standard set error (RFC 8620 §5.3): one or more properties were
155 /// invalid.
156 InvalidProperties {
157 /// Optional human-readable detail.
158 description: Option<String>,
159 /// The invalid property names.
160 #[serde(default)]
161 properties: Vec<String>,
162 },
163 /// Catch-all for set errors not modelled above.
164 #[serde(other)]
165 Unknown,
166}
167
168/// Failure causes during a JMAP `AddressBook/set` flow.
169#[derive(Debug, Error)]
170pub enum JmapAddressBookSetError {
171 /// The inner send coroutine failed.
172 #[error("JMAP AddressBook/set failed: {0}")]
173 Send(#[from] JmapSendError),
174 /// The method arguments could not be serialized.
175 #[error("JMAP AddressBook/set failed: serialize args: {0}")]
176 SerializeArgs(#[source] serde_json::Error),
177 /// The inner generic set coroutine failed.
178 #[error("JMAP AddressBook/set failed: {0}")]
179 Set(#[from] JmapSetError),
180}
181
182/// Arguments for an `AddressBook/set` request.
183#[derive(Clone, Debug, Default, Serialize)]
184#[serde(rename_all = "camelCase")]
185pub struct JmapAddressBookSetArgs {
186 /// Objects to create (client ID → partial AddressBook object).
187 #[serde(skip_serializing_if = "Option::is_none")]
188 pub create: Option<BTreeMap<String, JmapAddressBookCreate>>,
189 /// Objects to update (AddressBook ID → patch object).
190 #[serde(skip_serializing_if = "Option::is_none")]
191 pub update: Option<BTreeMap<String, JmapAddressBookUpdate>>,
192 /// IDs of objects to destroy.
193 #[serde(skip_serializing_if = "Option::is_none")]
194 pub destroy: Option<Vec<String>>,
195 /// Whether to remove contained ContactCards when destroying an
196 /// AddressBook; a card left in no AddressBook is destroyed
197 /// (RFC 9610 §2.3).
198 #[serde(skip_serializing_if = "Option::is_none")]
199 pub on_destroy_remove_contents: Option<bool>,
200 /// AddressBook ID (or `#`-prefixed creation ID) to make the default
201 /// when all changes succeed (RFC 9610 §2.3).
202 #[serde(skip_serializing_if = "Option::is_none")]
203 pub on_success_set_is_default: Option<String>,
204}
205
206/// Successful terminal output of [`JmapAddressBookSet`].
207#[derive(Clone, Debug)]
208pub struct JmapAddressBookSetOutput {
209 /// The new server state after the call.
210 pub new_state: String,
211 /// The created address books, keyed by client id.
212 pub created: BTreeMap<String, JmapAddressBook>,
213 /// The updated address books, keyed by id.
214 pub updated: BTreeMap<String, Option<JmapAddressBook>>,
215 /// Ids of the destroyed objects.
216 pub destroyed: Vec<String>,
217 /// The failed creates, keyed by client id.
218 pub not_created: BTreeMap<String, JmapAddressBookSetItemError>,
219 /// The failed updates, keyed by id.
220 pub not_updated: BTreeMap<String, JmapAddressBookSetItemError>,
221 /// The failed destroys, keyed by id.
222 pub not_destroyed: BTreeMap<String, JmapAddressBookSetItemError>,
223 /// Whether the server indicated the connection can be reused.
224 pub keep_alive: bool,
225}
226
227/// I/O-free coroutine for the JMAP `AddressBook/set` method.
228pub struct JmapAddressBookSet {
229 state: State,
230}
231
232impl JmapAddressBookSet {
233 /// Prepares the method call request and builds the coroutine.
234 pub fn new(
235 session: &JmapSession,
236 http_auth: &SecretString,
237 args: JmapAddressBookSetArgs,
238 ) -> Result<Self, JmapAddressBookSetError> {
239 let account_id = session
240 .primary_accounts
241 .get(JMAP_CONTACTS_CAPABILITY)
242 .cloned()
243 .unwrap_or_default();
244 let api_url = &session.api_url;
245
246 let json_args = serde_json::to_value(AddressBookSetRequest { account_id, args })
247 .map_err(JmapAddressBookSetError::SerializeArgs)?;
248
249 let mut batch = JmapBatch::new();
250 batch.add("AddressBook/set", json_args);
251 let request = batch.into_request(vec![
252 JMAP_CORE_CAPABILITY.into(),
253 JMAP_CONTACTS_CAPABILITY.into(),
254 ]);
255
256 let send = JmapSend::new(http_auth, api_url, request)?;
257 Ok(Self {
258 state: State::Set(JmapSet::from_send(send)),
259 })
260 }
261}
262
263impl JmapCoroutine for JmapAddressBookSet {
264 type Yield = JmapYield;
265 type Return = Result<JmapAddressBookSetOutput, JmapAddressBookSetError>;
266
267 fn resume(&mut self, arg: Option<&[u8]>) -> JmapCoroutineState<Self::Yield, Self::Return> {
268 match &mut self.state {
269 State::Set(set) => {
270 let JmapSetOutput {
271 new_state,
272 created,
273 updated,
274 destroyed,
275 not_created,
276 not_updated,
277 not_destroyed,
278 keep_alive,
279 } = jmap_try!(set, arg);
280 let parse = |map: BTreeMap<String, serde_json::Value>| {
281 map.into_iter()
282 .map(|(k, v)| {
283 let e = serde_json::from_value(v)
284 .unwrap_or(JmapAddressBookSetItemError::Unknown);
285 (k, e)
286 })
287 .collect()
288 };
289 JmapCoroutineState::Complete(Ok(JmapAddressBookSetOutput {
290 new_state,
291 created,
292 updated,
293 destroyed,
294 not_created: parse(not_created),
295 not_updated: parse(not_updated),
296 not_destroyed: parse(not_destroyed),
297 keep_alive,
298 }))
299 }
300 }
301 }
302}
303
304enum State {
305 Set(JmapSet<JmapAddressBook>),
306}
307
308#[derive(Serialize)]
309struct AddressBookSetRequest {
310 #[serde(rename = "accountId")]
311 account_id: String,
312 #[serde(flatten)]
313 args: JmapAddressBookSetArgs,
314}