1use crate::Id;
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize, Deserialize)]
12#[error("{error_type}")]
13#[non_exhaustive]
14pub struct JmapError {
15 #[serde(rename = "type")]
17 pub error_type: String,
18 #[serde(skip_serializing_if = "Option::is_none")]
20 pub description: Option<String>,
21 #[serde(rename = "existingId", skip_serializing_if = "Option::is_none")]
23 pub existing_id: Option<Id>,
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub limit: Option<u64>,
27}
28
29impl JmapError {
30 pub fn invalid_arguments(desc: impl Into<String>) -> Self {
32 Self {
33 error_type: "invalidArguments".into(),
34 description: Some(desc.into()),
35 existing_id: None,
36 limit: None,
37 }
38 }
39
40 pub fn forbidden() -> Self {
42 Self {
43 error_type: "forbidden".into(),
44 description: None,
45 existing_id: None,
46 limit: None,
47 }
48 }
49
50 pub fn not_found() -> Self {
52 Self {
53 error_type: "notFound".into(),
54 description: None,
55 existing_id: None,
56 limit: None,
57 }
58 }
59
60 pub fn account_not_found() -> Self {
62 Self {
63 error_type: "accountNotFound".into(),
64 description: None,
65 existing_id: None,
66 limit: None,
67 }
68 }
69
70 pub fn account_not_supported_by_method() -> Self {
72 Self {
73 error_type: "accountNotSupportedByMethod".into(),
74 description: None,
75 existing_id: None,
76 limit: None,
77 }
78 }
79
80 pub fn account_read_only() -> Self {
82 Self {
83 error_type: "accountReadOnly".into(),
84 description: None,
85 existing_id: None,
86 limit: None,
87 }
88 }
89
90 pub fn server_unavailable() -> Self {
92 Self {
93 error_type: "serverUnavailable".into(),
94 description: None,
95 existing_id: None,
96 limit: None,
97 }
98 }
99
100 pub fn server_fail(desc: impl Into<String>) -> Self {
102 Self {
103 error_type: "serverFail".into(),
104 description: Some(desc.into()),
105 existing_id: None,
106 limit: None,
107 }
108 }
109
110 pub fn server_partial_fail() -> Self {
112 Self {
113 error_type: "serverPartialFail".into(),
114 description: None,
115 existing_id: None,
116 limit: None,
117 }
118 }
119
120 pub fn unknown_method() -> Self {
122 Self {
123 error_type: "unknownMethod".into(),
124 description: None,
125 existing_id: None,
126 limit: None,
127 }
128 }
129
130 pub fn invalid_result_reference() -> Self {
132 Self {
133 error_type: "invalidResultReference".into(),
134 description: None,
135 existing_id: None,
136 limit: None,
137 }
138 }
139
140 pub fn cannot_calculate_changes() -> Self {
142 Self {
143 error_type: "cannotCalculateChanges".into(),
144 description: None,
145 existing_id: None,
146 limit: None,
147 }
148 }
149
150 pub fn state_mismatch() -> Self {
152 Self {
153 error_type: "stateMismatch".into(),
154 description: None,
155 existing_id: None,
156 limit: None,
157 }
158 }
159
160 pub fn too_large() -> Self {
162 Self {
163 error_type: "tooLarge".into(),
164 description: None,
165 existing_id: None,
166 limit: None,
167 }
168 }
169
170 pub fn request_too_large() -> Self {
172 Self {
173 error_type: "requestTooLarge".into(),
174 description: None,
175 existing_id: None,
176 limit: None,
177 }
178 }
179
180 pub fn over_quota() -> Self {
182 Self {
183 error_type: "overQuota".into(),
184 description: None,
185 existing_id: None,
186 limit: None,
187 }
188 }
189
190 pub fn rate_limit() -> Self {
192 Self {
193 error_type: "rateLimit".into(),
194 description: None,
195 existing_id: None,
196 limit: None,
197 }
198 }
199
200 pub fn invalid_patch() -> Self {
202 Self {
203 error_type: "invalidPatch".into(),
204 description: None,
205 existing_id: None,
206 limit: None,
207 }
208 }
209
210 pub fn will_destroy() -> Self {
212 Self {
213 error_type: "willDestroy".into(),
214 description: None,
215 existing_id: None,
216 limit: None,
217 }
218 }
219
220 pub fn invalid_properties() -> Self {
222 Self {
223 error_type: "invalidProperties".into(),
224 description: None,
225 existing_id: None,
226 limit: None,
227 }
228 }
229
230 pub fn singleton() -> Self {
232 Self {
233 error_type: "singleton".into(),
234 description: None,
235 existing_id: None,
236 limit: None,
237 }
238 }
239
240 pub fn unsupported_filter() -> Self {
242 Self {
243 error_type: "unsupportedFilter".into(),
244 description: None,
245 existing_id: None,
246 limit: None,
247 }
248 }
249
250 pub fn anchor_not_found() -> Self {
252 Self {
253 error_type: "anchorNotFound".into(),
254 description: None,
255 existing_id: None,
256 limit: None,
257 }
258 }
259
260 pub fn already_exists(existing_id: Id) -> Self {
265 Self {
266 error_type: "alreadyExists".into(),
267 description: None,
268 existing_id: Some(existing_id),
269 limit: None,
270 }
271 }
272
273 pub fn from_account_not_found() -> Self {
275 Self {
276 error_type: "fromAccountNotFound".into(),
277 description: None,
278 existing_id: None,
279 limit: None,
280 }
281 }
282
283 pub fn from_account_not_supported_by_method() -> Self {
285 Self {
286 error_type: "fromAccountNotSupportedByMethod".into(),
287 description: None,
288 existing_id: None,
289 limit: None,
290 }
291 }
292
293 pub fn unsupported_sort() -> Self {
295 Self {
296 error_type: "unsupportedSort".into(),
297 description: None,
298 existing_id: None,
299 limit: None,
300 }
301 }
302
303 #[deprecated(
311 note = "always use too_many_changes_with_limit to include the limit per RFC 8620 §9.6.1"
312 )]
313 pub fn too_many_changes() -> Self {
314 Self {
315 error_type: "tooManyChanges".into(),
316 description: None,
317 existing_id: None,
318 limit: None,
319 }
320 }
321
322 pub fn too_many_changes_with_limit(limit: u64) -> Self {
327 Self {
328 error_type: "tooManyChanges".into(),
329 description: None,
330 existing_id: None,
331 limit: Some(limit),
332 }
333 }
334
335 pub fn not_json() -> Self {
339 Self {
340 error_type: "notJSON".into(),
341 description: None,
342 existing_id: None,
343 limit: None,
344 }
345 }
346
347 pub fn not_request() -> Self {
351 Self {
352 error_type: "notRequest".into(),
353 description: None,
354 existing_id: None,
355 limit: None,
356 }
357 }
358
359 pub fn limit(limit_name: impl Into<String>) -> Self {
375 Self {
376 error_type: "limit".into(),
377 description: Some(limit_name.into()),
378 existing_id: None,
379 limit: None,
380 }
381 }
382
383 #[deprecated(
390 note = "always use unknown_capability_with_detail to include the URI in the error"
391 )]
392 pub fn unknown_capability() -> Self {
393 Self {
394 error_type: "unknownCapability".into(),
395 description: None,
396 existing_id: None,
397 limit: None,
398 }
399 }
400
401 pub fn unknown_capability_with_detail(uri: impl Into<String>) -> Self {
415 Self {
416 error_type: "unknownCapability".into(),
417 description: Some(uri.into()),
418 existing_id: None,
419 limit: None,
420 }
421 }
422
423 pub fn custom(error_type: impl Into<String>) -> Self {
429 Self {
430 error_type: error_type.into(),
431 description: None,
432 existing_id: None,
433 limit: None,
434 }
435 }
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441
442 #[test]
445 fn invalid_arguments_serializes_type_and_description() {
446 let e = JmapError::invalid_arguments("ids field is required");
447 let json = serde_json::to_string(&e).unwrap();
448 assert!(
449 json.contains("\"type\""),
450 "must use 'type' key per RFC 8620"
451 );
452 assert!(json.contains("\"invalidArguments\""));
453 assert!(json.contains("\"description\""));
454 assert!(json.contains("ids field is required"));
455 }
456
457 #[test]
458 fn forbidden_omits_description() {
459 let e = JmapError::forbidden();
460 let json = serde_json::to_string(&e).unwrap();
461 assert!(json.contains("\"forbidden\""));
462 assert!(
463 !json.contains("\"description\""),
464 "None description must be omitted"
465 );
466 }
467
468 #[test]
469 fn not_found_type_string() {
470 let e = JmapError::not_found();
471 let json = serde_json::to_string(&e).unwrap();
472 assert!(json.contains("\"notFound\""));
473 assert!(!json.contains("\"description\""));
474 }
475
476 #[test]
477 fn account_not_found_type_string() {
478 let e = JmapError::account_not_found();
479 let json = serde_json::to_string(&e).unwrap();
480 assert!(json.contains("\"accountNotFound\""));
481 assert!(!json.contains("\"description\""));
482 }
483
484 #[test]
485 fn account_not_supported_by_method_type_string() {
486 let e = JmapError::account_not_supported_by_method();
487 let json = serde_json::to_string(&e).unwrap();
488 assert!(json.contains("\"accountNotSupportedByMethod\""));
489 }
490
491 #[test]
492 fn account_read_only_type_string() {
493 let e = JmapError::account_read_only();
494 let json = serde_json::to_string(&e).unwrap();
495 assert!(json.contains("\"accountReadOnly\""));
496 }
497
498 #[test]
499 fn server_unavailable_type_string() {
500 let e = JmapError::server_unavailable();
501 let json = serde_json::to_string(&e).unwrap();
502 assert!(json.contains("\"serverUnavailable\""));
503 }
504
505 #[test]
506 fn server_fail_includes_description() {
507 let e = JmapError::server_fail("internal error");
508 let json = serde_json::to_string(&e).unwrap();
509 assert!(json.contains("\"serverFail\""));
510 assert!(json.contains("internal error"));
511 }
512
513 #[test]
514 fn server_partial_fail_type_string() {
515 let e = JmapError::server_partial_fail();
516 let json = serde_json::to_string(&e).unwrap();
517 assert!(json.contains("\"serverPartialFail\""));
518 }
519
520 #[test]
521 fn unknown_method_type_string() {
522 let e = JmapError::unknown_method();
523 let json = serde_json::to_string(&e).unwrap();
524 assert!(json.contains("\"unknownMethod\""));
525 }
526
527 #[test]
528 fn invalid_result_reference_type_string() {
529 let e = JmapError::invalid_result_reference();
530 let json = serde_json::to_string(&e).unwrap();
531 assert!(json.contains("\"invalidResultReference\""));
532 }
533
534 #[test]
535 fn cannot_calculate_changes_type_string() {
536 let e = JmapError::cannot_calculate_changes();
537 let json = serde_json::to_string(&e).unwrap();
538 assert!(json.contains("\"cannotCalculateChanges\""));
539 }
540
541 #[test]
542 fn state_mismatch_type_string() {
543 let e = JmapError::state_mismatch();
544 let json = serde_json::to_string(&e).unwrap();
545 assert!(json.contains("\"stateMismatch\""));
546 }
547
548 #[test]
549 fn too_large_type_string() {
550 let e = JmapError::too_large();
551 let json = serde_json::to_string(&e).unwrap();
552 assert!(json.contains("\"tooLarge\""));
553 }
554
555 #[test]
556 fn request_too_large_type_string() {
557 let e = JmapError::request_too_large();
558 let json = serde_json::to_string(&e).unwrap();
559 assert!(json.contains("\"requestTooLarge\""));
560 assert!(!json.contains("\"description\""));
561 }
562
563 #[test]
564 fn over_quota_type_string() {
565 let e = JmapError::over_quota();
566 let json = serde_json::to_string(&e).unwrap();
567 assert!(json.contains("\"overQuota\""));
568 }
569
570 #[test]
571 fn rate_limit_type_string() {
572 let e = JmapError::rate_limit();
573 let json = serde_json::to_string(&e).unwrap();
574 assert!(json.contains("\"rateLimit\""));
575 }
576
577 #[test]
578 fn invalid_patch_type_string() {
579 let e = JmapError::invalid_patch();
580 let json = serde_json::to_string(&e).unwrap();
581 assert!(json.contains("\"invalidPatch\""));
582 }
583
584 #[test]
585 fn will_destroy_type_string() {
586 let e = JmapError::will_destroy();
587 let json = serde_json::to_string(&e).unwrap();
588 assert!(json.contains("\"willDestroy\""));
589 }
590
591 #[test]
592 fn invalid_properties_type_string() {
593 let e = JmapError::invalid_properties();
594 let json = serde_json::to_string(&e).unwrap();
595 assert!(json.contains("\"invalidProperties\""));
596 }
597
598 #[test]
599 fn singleton_type_string() {
600 let e = JmapError::singleton();
601 let json = serde_json::to_string(&e).unwrap();
602 assert!(json.contains("\"singleton\""));
603 }
604
605 #[test]
606 fn unsupported_filter_type_string() {
607 let e = JmapError::unsupported_filter();
608 let json = serde_json::to_string(&e).unwrap();
609 assert!(json.contains("\"unsupportedFilter\""));
610 }
611
612 #[test]
613 fn anchor_not_found_type_string() {
614 let e = JmapError::anchor_not_found();
615 let json = serde_json::to_string(&e).unwrap();
616 assert!(json.contains("\"anchorNotFound\""));
617 }
618
619 #[test]
621 fn already_exists_includes_existing_id() {
622 let e = JmapError::already_exists(Id::from("abc123"));
623 let json = serde_json::to_string(&e).unwrap();
624 assert!(json.contains("\"alreadyExists\""));
625 assert!(json.contains("\"existingId\""));
626 assert!(json.contains("\"abc123\""));
627 assert!(!json.contains("\"description\""));
628 }
629
630 #[test]
631 fn from_account_not_found_type_string() {
632 let e = JmapError::from_account_not_found();
633 let json = serde_json::to_string(&e).unwrap();
634 assert!(json.contains("\"fromAccountNotFound\""));
635 assert!(!json.contains("\"description\""));
636 }
637
638 #[test]
639 fn from_account_not_supported_by_method_type_string() {
640 let e = JmapError::from_account_not_supported_by_method();
641 let json = serde_json::to_string(&e).unwrap();
642 assert!(json.contains("\"fromAccountNotSupportedByMethod\""));
643 assert!(!json.contains("\"description\""));
644 }
645
646 #[test]
647 fn unsupported_sort_type_string() {
648 let e = JmapError::unsupported_sort();
649 let json = serde_json::to_string(&e).unwrap();
650 assert!(json.contains("\"unsupportedSort\""));
651 assert!(!json.contains("\"description\""));
652 }
653
654 #[allow(deprecated)]
655 #[test]
656 fn too_many_changes_type_string() {
657 let e = JmapError::too_many_changes();
658 let json = serde_json::to_string(&e).unwrap();
659 assert!(json.contains("\"tooManyChanges\""));
660 assert!(!json.contains("\"description\""));
661 }
662
663 #[test]
664 fn too_many_changes_with_limit_serializes_limit_field() {
665 let err = JmapError::too_many_changes_with_limit(100);
666 let v = serde_json::to_value(&err).unwrap();
667 assert_eq!(v["type"], "tooManyChanges");
668 assert_eq!(v["limit"], 100u64);
669 assert!(v.get("description").is_none());
670 }
671
672 #[allow(deprecated)]
679 #[test]
680 fn too_many_changes_without_limit_has_no_limit_field() {
681 let err = JmapError::too_many_changes();
682 let v = serde_json::to_value(&err).unwrap();
683 assert_eq!(v["type"], "tooManyChanges");
684 assert!(v.get("limit").is_none());
685 }
686
687 #[test]
688 fn not_json_type_string() {
689 let e = JmapError::not_json();
690 let json = serde_json::to_string(&e).unwrap();
691 assert!(json.contains("\"notJSON\""));
692 assert!(!json.contains("\"description\""));
693 }
694
695 #[test]
696 fn not_request_type_string() {
697 let e = JmapError::not_request();
698 let json = serde_json::to_string(&e).unwrap();
699 assert!(json.contains("\"notRequest\""));
700 assert!(!json.contains("\"description\""));
701 }
702
703 #[test]
704 fn limit_includes_limit_name_in_description() {
705 let e = JmapError::limit("maxCallsInRequest");
708 let json = serde_json::to_string(&e).unwrap();
709 assert!(json.contains("\"limit\""));
710 assert!(json.contains("\"maxCallsInRequest\""));
711 }
712
713 #[allow(deprecated)]
714 #[test]
715 fn unknown_capability_type_string() {
716 let e = JmapError::unknown_capability();
717 let json = serde_json::to_string(&e).unwrap();
718 assert!(json.contains("\"unknownCapability\""));
719 assert!(!json.contains("\"description\""));
720 }
721
722 #[test]
724 fn unknown_capability_with_detail_includes_uri() {
725 let e = JmapError::unknown_capability_with_detail("urn:example:unknown");
726 assert_eq!(e.error_type, "unknownCapability");
727 assert_eq!(e.description.as_deref(), Some("urn:example:unknown"));
728 }
729
730 #[test]
731 fn custom_error_type_round_trips() {
732 let e = JmapError::custom("urn:example:customError");
733 assert_eq!(e.error_type, "urn:example:customError");
734 let json = serde_json::to_string(&e).unwrap();
735 assert!(json.contains("\"urn:example:customError\""));
736 let restored: JmapError = serde_json::from_str(&json).unwrap();
737 assert_eq!(restored.error_type, "urn:example:customError");
738 }
739
740 #[test]
741 fn round_trip_deserialize() {
742 let original = JmapError::invalid_arguments("test");
744 let json = serde_json::to_string(&original).unwrap();
745 let restored: JmapError = serde_json::from_str(&json).unwrap();
746 assert_eq!(restored.error_type, "invalidArguments");
747 assert_eq!(restored.description.as_deref(), Some("test"));
748 }
749
750 #[test]
752 fn fixture_response_contains_unknown_method_error() {
753 let raw = include_str!("../tests/fixtures/rfc8620-response.json");
754 let v: serde_json::Value = serde_json::from_str(raw).expect("parse fixture");
755 let inv = &v["methodResponses"][3];
756 assert_eq!(inv[0], "error");
757 assert_eq!(inv[1]["type"], "unknownMethod");
758 assert_eq!(inv[2], "c3");
759 }
760}