1use core::{fmt, marker::PhantomData};
36
37use alloc::{collections::BTreeMap, string::String, vec::Vec};
38
39use log::trace;
40use secrecy::SecretString;
41use serde::{Deserialize, Serialize, de::DeserializeOwned};
42use thiserror::Error;
43use url::Url;
44
45use crate::{
46 coroutine::*,
47 jmap_try,
48 rfc8620::{JmapBatch, JmapMethodError, send::*},
49};
50
51#[derive(Debug, Error)]
53pub enum JmapSetError {
54 #[error("JMAP Foo/set failed: missing response in method_responses")]
55 MissingResponse,
56 #[error("JMAP Foo/set failed: {0}")]
57 Send(#[from] JmapSendError),
58 #[error("JMAP Foo/set failed: serialize args: {0}")]
59 SerializeArgs(#[source] serde_json::Error),
60 #[error("JMAP Foo/set failed: parse response: {0}")]
61 ParseResponse(#[source] serde_json::Error),
62 #[error("JMAP Foo/set failed: {0}")]
63 Method(#[from] JmapMethodError),
64}
65
66#[derive(Clone, Debug)]
68pub struct JmapSetOptions<C: Serialize, U: Serialize> {
69 pub if_in_state: Option<String>,
72 pub create: Option<BTreeMap<String, C>>,
74 pub update: Option<BTreeMap<String, U>>,
76 pub destroy: Option<Vec<String>>,
78}
79
80impl<C: Serialize, U: Serialize> Default for JmapSetOptions<C, U> {
81 fn default() -> Self {
82 Self {
83 if_in_state: None,
84 create: None,
85 update: None,
86 destroy: None,
87 }
88 }
89}
90
91#[derive(Clone, Debug)]
93pub struct JmapSetOutput<T> {
94 pub new_state: String,
95 pub created: BTreeMap<String, T>,
96 pub updated: BTreeMap<String, Option<T>>,
97 pub destroyed: Vec<String>,
98 pub not_created: BTreeMap<String, serde_json::Value>,
99 pub not_updated: BTreeMap<String, serde_json::Value>,
100 pub not_destroyed: BTreeMap<String, serde_json::Value>,
101 pub keep_alive: bool,
102}
103
104pub struct JmapSet<T> {
106 state: State,
107 _phantom: PhantomData<T>,
108}
109
110impl<T: DeserializeOwned> JmapSet<T> {
111 pub fn new<C: Serialize, U: Serialize>(
113 account_id: String,
114 http_auth: &SecretString,
115 api_url: &Url,
116 method: impl Into<String>,
117 capabilities: Vec<String>,
118 opts: JmapSetOptions<C, U>,
119 ) -> Result<Self, JmapSetError> {
120 let args = serde_json::to_value(SetArgs {
121 account_id,
122 if_in_state: opts.if_in_state,
123 create: opts.create,
124 update: opts.update,
125 destroy: opts.destroy,
126 })
127 .map_err(JmapSetError::SerializeArgs)?;
128
129 let mut batch = JmapBatch::new();
130 batch.add(method, args);
131
132 let request = batch.into_request(capabilities);
133 let send = JmapSend::new(http_auth, api_url, request)?;
134
135 Ok(Self {
136 state: State::Send(send),
137 _phantom: PhantomData,
138 })
139 }
140
141 pub fn from_send(send: JmapSend) -> Self {
143 Self {
144 state: State::Send(send),
145 _phantom: PhantomData,
146 }
147 }
148}
149
150impl<T: DeserializeOwned> JmapCoroutine for JmapSet<T> {
151 type Yield = JmapYield;
152 type Return = Result<JmapSetOutput<T>, JmapSetError>;
153
154 fn resume(&mut self, arg: Option<&[u8]>) -> JmapCoroutineState<Self::Yield, Self::Return> {
155 trace!("set: {}", self.state);
156 match &mut self.state {
157 State::Send(send) => {
158 let JmapSendOutput {
159 response,
160 keep_alive,
161 } = jmap_try!(send, arg);
162
163 let Some((name, args, _)) = response.method_responses.into_iter().next() else {
164 return JmapCoroutineState::Complete(Err(JmapSetError::MissingResponse));
165 };
166
167 if name == "error" {
168 let err = serde_json::from_value::<JmapMethodError>(args)
169 .unwrap_or(JmapMethodError::Unknown);
170 return JmapCoroutineState::Complete(Err(err.into()));
171 }
172
173 match serde_json::from_value::<SetResponse<T>>(args) {
174 Ok(r) => JmapCoroutineState::Complete(Ok(JmapSetOutput {
175 new_state: r.new_state,
176 created: r.created.unwrap_or_default(),
177 updated: r.updated.unwrap_or_default(),
178 destroyed: r.destroyed.unwrap_or_default(),
179 not_created: r.not_created.unwrap_or_default(),
180 not_updated: r.not_updated.unwrap_or_default(),
181 not_destroyed: r.not_destroyed.unwrap_or_default(),
182 keep_alive,
183 })),
184 Err(err) => JmapCoroutineState::Complete(Err(JmapSetError::ParseResponse(err))),
185 }
186 }
187 }
188 }
189}
190
191enum State {
192 Send(JmapSend),
193}
194
195impl fmt::Display for State {
196 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197 match self {
198 Self::Send(_) => f.write_str("send"),
199 }
200 }
201}
202
203#[derive(Serialize)]
204#[serde(rename_all = "camelCase")]
205struct SetArgs<C: Serialize, U: Serialize> {
206 account_id: String,
207 #[serde(skip_serializing_if = "Option::is_none")]
208 if_in_state: Option<String>,
209 #[serde(skip_serializing_if = "Option::is_none")]
210 create: Option<BTreeMap<String, C>>,
211 #[serde(skip_serializing_if = "Option::is_none")]
212 update: Option<BTreeMap<String, U>>,
213 #[serde(skip_serializing_if = "Option::is_none")]
214 destroy: Option<Vec<String>>,
215}
216
217#[derive(Deserialize)]
218#[serde(rename_all = "camelCase")]
219struct SetResponse<T> {
220 new_state: String,
221 created: Option<BTreeMap<String, T>>,
222 updated: Option<BTreeMap<String, Option<T>>>,
223 destroyed: Option<Vec<String>>,
224 not_created: Option<BTreeMap<String, serde_json::Value>>,
225 not_updated: Option<BTreeMap<String, serde_json::Value>>,
226 not_destroyed: Option<BTreeMap<String, serde_json::Value>>,
227}
228
229#[cfg(test)]
230mod tests {
231 use alloc::{format, string::ToString, vec};
232
233 use super::*;
234
235 #[derive(Debug, Deserialize, Serialize, PartialEq)]
236 struct Probe {
237 id: String,
238 }
239
240 #[derive(Serialize)]
241 struct Create {
242 name: String,
243 }
244
245 fn make_auth() -> SecretString {
246 SecretString::from("Bearer test")
247 }
248
249 fn make_url() -> Url {
250 "https://api.example.com/jmap/".parse().unwrap()
251 }
252
253 fn make_set() -> JmapSet<Probe> {
254 let mut create = BTreeMap::new();
255 create.insert(
256 "c1".to_string(),
257 Create {
258 name: "Inbox".into(),
259 },
260 );
261 JmapSet::<Probe>::new::<_, Probe>(
262 "a1".to_string(),
263 &make_auth(),
264 &make_url(),
265 "Mailbox/set",
266 vec!["urn:ietf:params:jmap:mail".to_string()],
267 JmapSetOptions {
268 create: Some(create),
269 ..Default::default()
270 },
271 )
272 .unwrap()
273 }
274
275 fn build_http_reply(body: &[u8]) -> Vec<u8> {
276 let head = format!(
277 "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nContent-Type: application/json\r\n\r\n",
278 body.len()
279 );
280 let mut bytes = head.into_bytes();
281 bytes.extend_from_slice(body);
282 bytes
283 }
284
285 #[test]
286 fn success_returns_ok() {
287 let mut cor = make_set();
288 expect_wants_write(&mut cor, None);
289 expect_wants_read(&mut cor);
290
291 let body = br#"{
292 "methodResponses": [["Mailbox/set", {"newState":"s2","created":{"c1":{"id":"m1"}}}, "c0"]],
293 "sessionState": "s2"
294 }"#;
295 let reply = build_http_reply(body);
296 let out = expect_complete_ok(&mut cor, &reply);
297 assert_eq!(out.new_state, "s2");
298 assert_eq!(out.created["c1"], Probe { id: "m1".into() });
299 }
300
301 #[test]
302 fn method_error_returns_method_error() {
303 let mut cor = make_set();
304 expect_wants_write(&mut cor, None);
305 expect_wants_read(&mut cor);
306
307 let body = br#"{
308 "methodResponses": [["error", {"type":"stateMismatch"}, "c0"]],
309 "sessionState": "s1"
310 }"#;
311 let reply = build_http_reply(body);
312 let err = expect_complete_err(&mut cor, &reply);
313 assert!(matches!(err, JmapSetError::Method(_)));
314 }
315
316 #[test]
317 fn missing_response_returns_missing_response() {
318 let mut cor = make_set();
319 expect_wants_write(&mut cor, None);
320 expect_wants_read(&mut cor);
321
322 let reply = build_http_reply(br#"{"methodResponses":[], "sessionState":"s1"}"#);
323 let err = expect_complete_err(&mut cor, &reply);
324 assert!(matches!(err, JmapSetError::MissingResponse));
325 }
326
327 #[test]
328 fn parse_error_returns_parse_response() {
329 let mut cor = make_set();
330 expect_wants_write(&mut cor, None);
331 expect_wants_read(&mut cor);
332
333 let body = br#"{
334 "methodResponses": [["Mailbox/set", {"newState":42}, "c0"]],
335 "sessionState": "s1"
336 }"#;
337 let reply = build_http_reply(body);
338 let err = expect_complete_err(&mut cor, &reply);
339 assert!(matches!(err, JmapSetError::ParseResponse(_)));
340 }
341
342 #[test]
343 fn not_created_passthrough_succeeds() {
344 let mut cor = make_set();
345 expect_wants_write(&mut cor, None);
346 expect_wants_read(&mut cor);
347
348 let body = br#"{
349 "methodResponses": [["Mailbox/set", {"newState":"s2","notCreated":{"c1":{"type":"invalidArguments"}}}, "c0"]],
350 "sessionState": "s2"
351 }"#;
352 let reply = build_http_reply(body);
353 let out = expect_complete_ok(&mut cor, &reply);
354 assert!(out.not_created.contains_key("c1"));
355 }
356
357 fn expect_wants_write(cor: &mut JmapSet<Probe>, arg: Option<&[u8]>) -> Vec<u8> {
360 match cor.resume(arg) {
361 JmapCoroutineState::Yielded(JmapYield::WantsWrite(bytes)) => bytes,
362 state => panic!("expected WantsWrite, got {state:?}"),
363 }
364 }
365
366 fn expect_wants_read(cor: &mut JmapSet<Probe>) {
367 match cor.resume(None) {
368 JmapCoroutineState::Yielded(JmapYield::WantsRead) => {}
369 state => panic!("expected WantsRead, got {state:?}"),
370 }
371 }
372
373 fn expect_complete_ok(cor: &mut JmapSet<Probe>, reply: &[u8]) -> JmapSetOutput<Probe> {
374 match cor.resume(Some(reply)) {
375 JmapCoroutineState::Complete(Ok(out)) => out,
376 state => panic!("expected Complete(Ok), got {state:?}"),
377 }
378 }
379
380 fn expect_complete_err(cor: &mut JmapSet<Probe>, reply: &[u8]) -> JmapSetError {
381 match cor.resume(Some(reply)) {
382 JmapCoroutineState::Complete(Err(err)) => err,
383 state => panic!("expected Complete(Err), got {state:?}"),
384 }
385 }
386}