1#![forbid(unsafe_code)]
8
9pub use jmap_types::{
10 Argument, Id, Invocation, JmapError, JmapRequest, JmapResponse, ResultReference, State, UTCDate,
11};
12
13pub mod backend;
14pub mod handlers;
15pub(crate) mod helpers;
16
17pub use backend::{
18 AddedItem, BackendChangesError, ChangesResult, GetObject, JmapBackend, JmapObject,
19 QueryChangesResult, QueryObject, QueryResult, SetObject,
20};
21pub use handlers::{handle_changes, handle_get, handle_query, handle_query_changes};
22pub use helpers::{extract_account_id, not_found_json, now_utc_string, ser};
23
24mod parse;
25mod response;
26
27pub use parse::{parse_request, resolve_args};
28pub use response::{error_invocation, error_status, request_error, RequestError};
29
30use std::{collections::HashMap, fmt, future::Future, pin::Pin, sync::Arc};
31
32use serde_json::Value;
33use tokio::task;
34
35pub type HandlerFuture =
45 Pin<Box<dyn Future<Output = Result<(Value, Vec<Invocation>), JmapError>> + Send>>;
46
47pub trait JmapHandler<CallerCtx>: Send + Sync {
60 fn call(
68 &self,
69 method: String,
70 call_id: String,
71 args: Value,
72 caller: CallerCtx,
73 ) -> HandlerFuture;
74}
75
76pub struct Dispatcher<CallerCtx> {
92 handlers: HashMap<String, Arc<dyn JmapHandler<CallerCtx>>>,
93}
94
95impl<CallerCtx: Clone + Send + 'static> Dispatcher<CallerCtx> {
96 pub fn new() -> Self {
98 Self {
99 handlers: HashMap::new(),
100 }
101 }
102
103 pub fn register(
110 &mut self,
111 method: impl Into<String>,
112 handler: Arc<dyn JmapHandler<CallerCtx>>,
113 ) {
114 self.handlers.insert(method.into(), handler);
115 }
116
117 pub async fn dispatch(
135 &self,
136 request: JmapRequest,
137 caller: CallerCtx,
138 session_state: State,
139 ) -> JmapResponse {
140 let mut method_responses: Vec<Invocation> = Vec::with_capacity(request.method_calls.len());
141 let client_sent_created_ids = request.created_ids.is_some();
142 let mut created_ids: HashMap<Id, Id> = request.created_ids.unwrap_or_default();
143
144 for (method, mut args, call_id) in request.method_calls {
146 if let Err(e) = resolve_args(&mut args, &method_responses) {
148 method_responses.push(error_invocation(&call_id, e));
149 continue;
150 }
151
152 let handler = match self.handlers.get(&method) {
154 Some(h) => Arc::clone(h),
155 None => {
156 method_responses.push(error_invocation(&call_id, JmapError::unknown_method()));
157 continue;
158 }
159 };
160
161 let caller_clone = caller.clone();
162 let method_clone = method.clone();
163 let call_id_clone = call_id.clone();
164
165 let result: Result<
167 Result<(Value, Vec<Invocation>), JmapError>,
168 tokio::task::JoinError,
169 > = task::spawn(async move {
170 handler
171 .call(method_clone, call_id_clone, args, caller_clone)
172 .await
173 })
174 .await;
175
176 match result {
177 Ok(Ok((primary_value, extra_invocations))) => {
178 if client_sent_created_ids {
182 if let Some(map) = primary_value.get("created").and_then(|v| v.as_object())
183 {
184 for (client_id, created_obj) in map {
185 if let Some(id_val) = created_obj.get("id").and_then(|v| v.as_str())
191 {
192 created_ids.insert(client_id.as_str().into(), id_val.into());
193 }
194 }
195 }
196 }
197 method_responses.push((method, primary_value, call_id));
201 method_responses.extend(extra_invocations);
202 }
203 Ok(Err(e)) => {
204 method_responses.push(error_invocation(&call_id, e));
205 }
206 Err(join_err) => {
207 let desc = if join_err.is_cancelled() {
210 "task cancelled"
211 } else {
212 "internal error"
213 };
214 method_responses.push(error_invocation(&call_id, JmapError::server_fail(desc)));
215 }
216 }
217 }
218
219 let created_ids = client_sent_created_ids.then_some(created_ids);
220
221 JmapResponse::new(method_responses, session_state, created_ids)
222 }
223}
224
225impl<CallerCtx: Clone + Send + 'static> Default for Dispatcher<CallerCtx> {
226 fn default() -> Self {
227 Self::new()
228 }
229}
230
231impl<CallerCtx> fmt::Debug for Dispatcher<CallerCtx> {
232 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233 f.debug_struct("Dispatcher")
234 .field("methods", &self.handlers.keys())
235 .finish()
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use serde_json::{json, Value};
243 use std::sync::{Arc, Mutex};
244
245 #[allow(dead_code)]
249 fn assert_dispatcher_send_sync() {
250 fn check<T: Send + Sync>() {}
251 check::<Dispatcher<String>>();
252 check::<Dispatcher<()>>();
253 }
254
255 struct EchoHandler(Value);
261
262 impl<C: Clone + Send + 'static> JmapHandler<C> for EchoHandler {
263 fn call(
264 &self,
265 _method: String,
266 _call_id: String,
267 _args: Value,
268 _caller: C,
269 ) -> HandlerFuture {
270 let v = self.0.clone();
271 Box::pin(async move { Ok((v, vec![])) })
272 }
273 }
274
275 struct ErrorHandler(JmapError);
277
278 impl JmapHandler<String> for ErrorHandler {
279 fn call(
280 &self,
281 _method: String,
282 _call_id: String,
283 _args: Value,
284 _caller: String,
285 ) -> HandlerFuture {
286 let e = self.0.clone();
287 Box::pin(async move { Err(e) })
288 }
289 }
290
291 struct CaptureArgsHandler(Arc<Mutex<Option<Value>>>);
293
294 impl JmapHandler<String> for CaptureArgsHandler {
295 fn call(
296 &self,
297 _method: String,
298 _call_id: String,
299 args: Value,
300 _caller: String,
301 ) -> HandlerFuture {
302 let slot = self.0.clone();
303 Box::pin(async move {
304 *slot.lock().expect("test: mutex poisoned") = Some(args);
305 Ok((json!({}), vec![]))
306 })
307 }
308 }
309
310 struct CaptureCallerHandler(Arc<Mutex<Option<String>>>);
312
313 impl JmapHandler<String> for CaptureCallerHandler {
314 fn call(
315 &self,
316 _method: String,
317 _call_id: String,
318 _args: Value,
319 caller: String,
320 ) -> HandlerFuture {
321 let slot = self.0.clone();
322 Box::pin(async move {
323 *slot.lock().expect("test: mutex poisoned") = Some(caller);
324 Ok((json!({}), vec![]))
325 })
326 }
327 }
328
329 struct PanicHandler;
331
332 impl JmapHandler<String> for PanicHandler {
333 fn call(
334 &self,
335 _method: String,
336 _call_id: String,
337 _args: Value,
338 _caller: String,
339 ) -> HandlerFuture {
340 Box::pin(async move { panic!("deliberate test panic") })
341 }
342 }
343
344 fn single_call(method: &str, args: Value, call_id: &str) -> JmapRequest {
349 JmapRequest::new(
350 vec!["urn:ietf:params:jmap:core".into()],
351 vec![(method.into(), args, call_id.into())],
352 None,
353 )
354 }
355
356 #[tokio::test]
362 async fn unknown_method_returns_error_invocation() {
363 let d: Dispatcher<String> = Dispatcher::new();
364 let req = single_call("Foo/get", json!({}), "c0");
365 let resp = d.dispatch(req, "alice".into(), "s0".into()).await;
366 assert_eq!(resp.method_responses.len(), 1);
367 let (_, args, call_id) = &resp.method_responses[0];
368 assert_eq!(call_id, "c0");
369 assert_eq!(args["type"], "unknownMethod");
370 }
371
372 #[tokio::test]
374 async fn known_method_success() {
375 let mut d: Dispatcher<String> = Dispatcher::new();
376 d.register("Foo/get", Arc::new(EchoHandler(json!({"list": []}))));
377 let req = single_call("Foo/get", json!({}), "c1");
378 let resp = d.dispatch(req, "alice".into(), "s0".into()).await;
379 assert_eq!(resp.method_responses.len(), 1);
380 let (method, args, call_id) = &resp.method_responses[0];
381 assert_eq!(method, "Foo/get");
382 assert_eq!(call_id, "c1");
383 assert_eq!(args["list"], json!([]));
384 }
385
386 #[tokio::test]
388 async fn handler_returns_error() {
389 let mut d: Dispatcher<String> = Dispatcher::new();
390 d.register("Foo/get", Arc::new(ErrorHandler(JmapError::not_found())));
391 let req = single_call("Foo/get", json!({}), "c2");
392 let resp = d.dispatch(req, "alice".into(), "s0".into()).await;
393 assert_eq!(resp.method_responses.len(), 1);
394 let (_, args, _) = &resp.method_responses[0];
395 assert_eq!(args["type"], "notFound");
396 }
397
398 #[tokio::test]
400 async fn session_state_echoed() {
401 let d: Dispatcher<String> = Dispatcher::new();
402 let req = JmapRequest::new(vec!["urn:ietf:params:jmap:core".into()], vec![], None);
403 let resp = d.dispatch(req, "alice".into(), "my-state-123".into()).await;
404 assert_eq!(resp.session_state.as_ref(), "my-state-123");
405 }
406
407 #[tokio::test]
414 async fn mixed_batch_all_responses_in_order() {
415 let mut d: Dispatcher<String> = Dispatcher::new();
416 d.register("M/a", Arc::new(EchoHandler(json!({"ok": true}))));
417 let req = JmapRequest::new(
419 vec!["urn:ietf:params:jmap:core".into()],
420 vec![
421 ("M/a".into(), json!({}), "c0".into()),
422 ("M/b".into(), json!({}), "c1".into()),
423 ("M/a".into(), json!({}), "c2".into()),
424 ],
425 None,
426 );
427 let resp = d.dispatch(req, "alice".into(), "s0".into()).await;
428 assert_eq!(
429 resp.method_responses.len(),
430 3,
431 "all three calls must produce a response"
432 );
433 assert_eq!(resp.method_responses[0].2, "c0");
435 assert!(
436 resp.method_responses[0].1.get("type").is_none(),
437 "c0 must not be an error"
438 );
439 assert_eq!(resp.method_responses[1].2, "c1");
441 assert_eq!(resp.method_responses[1].1["type"], "unknownMethod");
442 assert_eq!(resp.method_responses[2].2, "c2");
444 assert!(
445 resp.method_responses[2].1.get("type").is_none(),
446 "c2 must not be an error"
447 );
448 }
449
450 #[tokio::test]
452 async fn error_does_not_abort_subsequent_calls() {
453 let mut d: Dispatcher<String> = Dispatcher::new();
454 d.register("M/ok", Arc::new(EchoHandler(json!({"ok": true}))));
455 d.register("M/err", Arc::new(ErrorHandler(JmapError::forbidden())));
456 let req = JmapRequest::new(
457 vec!["urn:ietf:params:jmap:core".into()],
458 vec![
459 ("M/err".into(), json!({}), "c0".into()),
460 ("M/ok".into(), json!({}), "c1".into()),
461 ],
462 None,
463 );
464 let resp = d.dispatch(req, "alice".into(), "s0".into()).await;
465 assert_eq!(resp.method_responses.len(), 2);
466 assert_eq!(resp.method_responses[0].1["type"], "forbidden");
467 assert!(
468 resp.method_responses[1].1.get("type").is_none(),
469 "second call must succeed"
470 );
471 }
472
473 #[tokio::test]
479 async fn panicking_handler_returns_server_fail() {
480 let mut d: Dispatcher<String> = Dispatcher::new();
481 d.register("Panic/now", Arc::new(PanicHandler));
482 let req = single_call("Panic/now", json!({}), "c0");
483 let resp = d.dispatch(req, "alice".into(), "s0".into()).await;
484 assert_eq!(resp.method_responses.len(), 1);
485 let (_, args, _) = &resp.method_responses[0];
486 assert_eq!(
487 args["type"], "serverFail",
488 "panicking handler must produce serverFail"
489 );
490 }
491
492 #[tokio::test]
494 async fn panic_message_not_in_response() {
495 let mut d: Dispatcher<String> = Dispatcher::new();
496 d.register("Panic/now", Arc::new(PanicHandler));
497 let req = single_call("Panic/now", json!({}), "c0");
498 let resp = d.dispatch(req, "alice".into(), "s0".into()).await;
499 let (_, args, _) = &resp.method_responses[0];
500 if let Some(desc) = args["description"].as_str() {
501 assert!(
502 !desc.contains("deliberate test panic"),
503 "panic message must not leak into response description"
504 );
505 }
506 }
507
508 #[tokio::test]
514 async fn result_reference_resolved_before_dispatch() {
515 let captured = Arc::new(Mutex::new(None::<Value>));
516 let mut d: Dispatcher<String> = Dispatcher::new();
517 d.register(
518 "Foo/get",
519 Arc::new(EchoHandler(json!({"list": [{"id": "item-1"}]}))),
520 );
521 d.register(
522 "Bar/query",
523 Arc::new(CaptureArgsHandler(Arc::clone(&captured))),
524 );
525 let req = JmapRequest::new(
526 vec!["urn:ietf:params:jmap:core".into()],
527 vec![
528 ("Foo/get".into(), json!({}), "c0".into()),
529 (
530 "Bar/query".into(),
531 json!({"#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/list/0/id"}}),
532 "c1".into(),
533 ),
534 ],
535 None,
536 );
537 let resp = d.dispatch(req, "alice".into(), "s0".into()).await;
538 assert_eq!(resp.method_responses.len(), 2);
539 assert!(
541 resp.method_responses[1].1.get("type").is_none(),
542 "Bar/query must succeed after ResultReference resolution"
543 );
544 let got = captured
546 .lock()
547 .unwrap()
548 .clone()
549 .expect("CaptureArgsHandler was not called");
550 assert_eq!(
551 got["ids"],
552 json!("item-1"),
553 "resolved value must be the string item-1"
554 );
555 assert!(
556 got.get("#ids").is_none(),
557 "#ids key must have been replaced"
558 );
559 }
560
561 #[tokio::test]
563 async fn result_reference_failure_stops_that_call() {
564 let d: Dispatcher<String> = Dispatcher::new();
565 let req = single_call(
566 "Foo/get",
567 json!({"#ids": {"resultOf": "nonexistent", "name": "Foo/get", "path": "/x"}}),
568 "c0",
569 );
570 let resp = d.dispatch(req, "alice".into(), "s0".into()).await;
571 assert_eq!(resp.method_responses.len(), 1);
572 let (_, args, _) = &resp.method_responses[0];
573 assert!(
574 args.get("type").is_some(),
575 "failed ResultReference must produce an error invocation"
576 );
577 }
578
579 #[tokio::test]
586 async fn created_ids_accumulated_from_set_response() {
587 let mut d: Dispatcher<String> = Dispatcher::new();
588 d.register(
589 "Foo/set",
590 Arc::new(EchoHandler(
591 json!({"created": {"client-1": {"id": "server-abc"}}}),
592 )),
593 );
594 let req = JmapRequest::new(
596 vec!["urn:ietf:params:jmap:core".into()],
597 vec![("Foo/set".into(), json!({}), "c0".into())],
598 Some(std::collections::HashMap::new()),
599 );
600 let resp = d.dispatch(req, "alice".into(), "s0".into()).await;
601 let ids = resp
602 .created_ids
603 .as_ref()
604 .expect("created_ids must be Some when client sent createdIds");
605 assert_eq!(
606 ids.get(&Id::from("client-1")),
607 Some(&Id::from("server-abc")),
608 "client-1 must map to server-abc"
609 );
610 }
611
612 #[tokio::test]
614 async fn created_ids_absent_when_no_set() {
615 let mut d: Dispatcher<String> = Dispatcher::new();
616 d.register("Foo/get", Arc::new(EchoHandler(json!({"list": []}))));
617 let req = single_call("Foo/get", json!({}), "c0");
618 let resp = d.dispatch(req, "alice".into(), "s0".into()).await;
619 assert!(
620 resp.created_ids.is_none(),
621 "created_ids must be None when no /set call created objects"
622 );
623 }
624
625 #[tokio::test]
627 async fn created_ids_accumulated_across_multiple_set_calls() {
628 let mut d: Dispatcher<String> = Dispatcher::new();
629 d.register(
630 "A/set",
631 Arc::new(EchoHandler(json!({"created": {"cA": {"id": "sA"}}}))),
632 );
633 d.register(
634 "B/set",
635 Arc::new(EchoHandler(json!({"created": {"cB": {"id": "sB"}}}))),
636 );
637 let req = JmapRequest::new(
639 vec!["urn:ietf:params:jmap:core".into()],
640 vec![
641 ("A/set".into(), json!({}), "c0".into()),
642 ("B/set".into(), json!({}), "c1".into()),
643 ],
644 Some(std::collections::HashMap::new()),
645 );
646 let resp = d.dispatch(req, "alice".into(), "s0".into()).await;
647 let ids = resp
648 .created_ids
649 .as_ref()
650 .expect("created_ids must be Some when client sent createdIds");
651 assert_eq!(
652 ids.get(&Id::from("cA")),
653 Some(&Id::from("sA")),
654 "cA must be present"
655 );
656 assert_eq!(
657 ids.get(&Id::from("cB")),
658 Some(&Id::from("sB")),
659 "cB must be present"
660 );
661 }
662
663 #[tokio::test]
666 async fn created_ids_merges_with_pre_populated_map() {
667 let mut d: Dispatcher<String> = Dispatcher::new();
668 d.register(
669 "Foo/set",
670 Arc::new(EchoHandler(
671 json!({"created": {"client-new": {"id": "server-new"}}}),
672 )),
673 );
674 let mut initial = std::collections::HashMap::new();
676 initial.insert(Id::from("client-old"), Id::from("server-old"));
677 let req = JmapRequest::new(
678 vec!["urn:ietf:params:jmap:core".into()],
679 vec![("Foo/set".into(), json!({}), "c0".into())],
680 Some(initial),
681 );
682 let resp = d.dispatch(req, "alice".into(), "s0".into()).await;
683 let ids = resp
684 .created_ids
685 .as_ref()
686 .expect("created_ids must be Some when client sent createdIds");
687 assert_eq!(
688 ids.get(&Id::from("client-old")),
689 Some(&Id::from("server-old")),
690 "pre-populated entry must be preserved"
691 );
692 assert_eq!(
693 ids.get(&Id::from("client-new")),
694 Some(&Id::from("server-new")),
695 "new /set entry must be merged in"
696 );
697 }
698
699 #[tokio::test]
705 async fn caller_ctx_passed_to_handler() {
706 let captured = Arc::new(Mutex::new(None::<String>));
707 let mut d: Dispatcher<String> = Dispatcher::new();
708 d.register(
709 "Foo/get",
710 Arc::new(CaptureCallerHandler(Arc::clone(&captured))),
711 );
712 let req = single_call("Foo/get", json!({}), "c0");
713 let resp = d.dispatch(req, "alice".into(), "s0".into()).await;
714 assert!(
715 resp.method_responses[0].1.get("type").is_none(),
716 "must succeed"
717 );
718 let got = captured
719 .lock()
720 .unwrap()
721 .clone()
722 .expect("handler was not called");
723 assert_eq!(got, "alice", "caller must be passed through unchanged");
724 }
725
726 #[tokio::test]
728 async fn unit_caller_ctx_works() {
729 let mut d: Dispatcher<()> = Dispatcher::new();
730 d.register("Foo/get", Arc::new(EchoHandler(json!({"ok": true}))));
731 let req = single_call("Foo/get", json!({}), "c0");
732 let resp = d.dispatch(req, (), "s0".into()).await;
733 assert_eq!(resp.method_responses.len(), 1);
734 assert!(
735 resp.method_responses[0].1.get("type").is_none(),
736 "must succeed with () caller"
737 );
738 }
739
740 struct ExtraInvocationHandler;
749
750 impl JmapHandler<String> for ExtraInvocationHandler {
751 fn call(
752 &self,
753 _method: String,
754 _call_id: String,
755 _args: Value,
756 _caller: String,
757 ) -> HandlerFuture {
758 Box::pin(async move {
759 let primary = json!({"type": "primary"});
760 let extra: Vec<Invocation> = vec![(
761 "Extra/call".to_owned(),
762 json!({"type": "extra"}),
763 "x0".to_owned(),
764 )];
765 Ok((primary, extra))
766 })
767 }
768 }
769
770 #[tokio::test]
773 async fn extra_invocations_appended_after_primary() {
774 let mut d: Dispatcher<String> = Dispatcher::new();
775 d.register("Sub/set", Arc::new(ExtraInvocationHandler));
776 let req = single_call("Sub/set", json!({}), "c0");
777 let resp = d.dispatch(req, "alice".into(), "s0".into()).await;
778
779 assert_eq!(
780 resp.method_responses.len(),
781 2,
782 "primary + 1 extra = 2 total invocations"
783 );
784 assert_eq!(resp.method_responses[0].0, "Sub/set");
786 assert_eq!(resp.method_responses[0].2, "c0");
787 assert_eq!(resp.method_responses[0].1["type"], "primary");
788 assert_eq!(resp.method_responses[1].0, "Extra/call");
790 assert_eq!(resp.method_responses[1].2, "x0");
791 assert_eq!(resp.method_responses[1].1["type"], "extra");
792 }
793}