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