1use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7#[non_exhaustive]
19#[derive(Debug, Clone, PartialEq, Eq, Hash)]
20pub enum MethodErrorType {
21 UnknownCapability,
23 NotJson,
26 NotRequest,
29 Limit,
32 ServerUnavailable,
35 ServerFail,
37 ServerPartialFail,
39 UnknownMethod,
41 InvalidArguments,
43 InvalidResultReference,
45 Forbidden,
47 AccountNotFound,
50 AccountNotSupportedByMethod,
52 AccountReadOnly,
54 RequestTooLarge,
56 CannotCalculateChanges,
59 StateMismatch,
62 FromAccountNotFound,
65 FromAccountNotSupportedByMethod,
68 AnchorNotFound,
70 UnsupportedSort,
72 UnsupportedFilter,
74 TooManyChanges,
76 Other(String),
78}
79
80crate::impl_string_enum!(
81 MethodErrorType,
82 "a JMAP method error type string",
83 "unknownCapability" => UnknownCapability,
84 "notJSON" => NotJson,
85 "notRequest" => NotRequest,
86 "limit" => Limit,
87 "serverUnavailable" => ServerUnavailable,
88 "serverFail" => ServerFail,
89 "serverPartialFail" => ServerPartialFail,
90 "unknownMethod" => UnknownMethod,
91 "invalidArguments" => InvalidArguments,
92 "invalidResultReference" => InvalidResultReference,
93 "forbidden" => Forbidden,
94 "accountNotFound" => AccountNotFound,
95 "accountNotSupportedByMethod" => AccountNotSupportedByMethod,
96 "accountReadOnly" => AccountReadOnly,
97 "requestTooLarge" => RequestTooLarge,
98 "cannotCalculateChanges" => CannotCalculateChanges,
99 "stateMismatch" => StateMismatch,
100 "fromAccountNotFound" => FromAccountNotFound,
101 "fromAccountNotSupportedByMethod" => FromAccountNotSupportedByMethod,
102 "anchorNotFound" => AnchorNotFound,
103 "unsupportedSort" => UnsupportedSort,
104 "unsupportedFilter" => UnsupportedFilter,
105 "tooManyChanges" => TooManyChanges,
106);
107
108#[non_exhaustive]
113#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize, Deserialize)]
114#[error("{error_type}")]
115pub struct JmapError {
116 #[serde(rename = "type")]
118 pub error_type: MethodErrorType,
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub description: Option<String>,
122 #[serde(skip_serializing_if = "Option::is_none")]
124 pub limit: Option<u64>,
125 #[serde(skip)]
134 pub limit_name: Option<String>,
135}
136
137impl JmapError {
138 pub fn invalid_arguments(desc: impl Into<String>) -> Self {
140 Self {
141 error_type: MethodErrorType::InvalidArguments,
142 description: Some(desc.into()),
143 limit: None,
144 limit_name: None,
145 }
146 }
147
148 pub fn forbidden() -> Self {
150 Self {
151 error_type: MethodErrorType::Forbidden,
152 description: None,
153 limit: None,
154 limit_name: None,
155 }
156 }
157
158 pub fn account_not_found() -> Self {
160 Self {
161 error_type: MethodErrorType::AccountNotFound,
162 description: None,
163 limit: None,
164 limit_name: None,
165 }
166 }
167
168 pub fn account_not_supported_by_method() -> Self {
170 Self {
171 error_type: MethodErrorType::AccountNotSupportedByMethod,
172 description: None,
173 limit: None,
174 limit_name: None,
175 }
176 }
177
178 pub fn account_read_only() -> Self {
180 Self {
181 error_type: MethodErrorType::AccountReadOnly,
182 description: None,
183 limit: None,
184 limit_name: None,
185 }
186 }
187
188 pub fn server_unavailable() -> Self {
190 Self {
191 error_type: MethodErrorType::ServerUnavailable,
192 description: None,
193 limit: None,
194 limit_name: None,
195 }
196 }
197
198 pub fn server_fail(desc: impl Into<String>) -> Self {
200 Self {
201 error_type: MethodErrorType::ServerFail,
202 description: Some(desc.into()),
203 limit: None,
204 limit_name: None,
205 }
206 }
207
208 pub fn server_partial_fail() -> Self {
210 Self {
211 error_type: MethodErrorType::ServerPartialFail,
212 description: None,
213 limit: None,
214 limit_name: None,
215 }
216 }
217
218 pub fn unknown_method() -> Self {
220 Self {
221 error_type: MethodErrorType::UnknownMethod,
222 description: None,
223 limit: None,
224 limit_name: None,
225 }
226 }
227
228 pub fn invalid_result_reference() -> Self {
230 Self {
231 error_type: MethodErrorType::InvalidResultReference,
232 description: None,
233 limit: None,
234 limit_name: None,
235 }
236 }
237
238 pub fn cannot_calculate_changes() -> Self {
240 Self {
241 error_type: MethodErrorType::CannotCalculateChanges,
242 description: None,
243 limit: None,
244 limit_name: None,
245 }
246 }
247
248 pub fn state_mismatch() -> Self {
250 Self {
251 error_type: MethodErrorType::StateMismatch,
252 description: None,
253 limit: None,
254 limit_name: None,
255 }
256 }
257
258 pub fn too_large() -> Self {
262 Self {
263 error_type: MethodErrorType::from("tooLarge"),
264 description: None,
265 limit: None,
266 limit_name: None,
267 }
268 }
269
270 pub fn request_too_large() -> Self {
272 Self {
273 error_type: MethodErrorType::RequestTooLarge,
274 description: None,
275 limit: None,
276 limit_name: None,
277 }
278 }
279
280 pub fn rate_limit() -> Self {
284 Self {
285 error_type: MethodErrorType::from("rateLimit"),
286 description: None,
287 limit: None,
288 limit_name: None,
289 }
290 }
291
292 pub fn unsupported_filter() -> Self {
294 Self {
295 error_type: MethodErrorType::UnsupportedFilter,
296 description: None,
297 limit: None,
298 limit_name: None,
299 }
300 }
301
302 pub fn anchor_not_found() -> Self {
304 Self {
305 error_type: MethodErrorType::AnchorNotFound,
306 description: None,
307 limit: None,
308 limit_name: None,
309 }
310 }
311
312 pub fn from_account_not_found() -> Self {
314 Self {
315 error_type: MethodErrorType::FromAccountNotFound,
316 description: None,
317 limit: None,
318 limit_name: None,
319 }
320 }
321
322 pub fn from_account_not_supported_by_method() -> Self {
324 Self {
325 error_type: MethodErrorType::FromAccountNotSupportedByMethod,
326 description: None,
327 limit: None,
328 limit_name: None,
329 }
330 }
331
332 pub fn unsupported_sort() -> Self {
334 Self {
335 error_type: MethodErrorType::UnsupportedSort,
336 description: None,
337 limit: None,
338 limit_name: None,
339 }
340 }
341
342 pub fn too_many_changes_with_limit(limit: u64) -> Self {
347 Self {
348 error_type: MethodErrorType::TooManyChanges,
349 description: None,
350 limit: Some(limit),
351 limit_name: None,
352 }
353 }
354
355 pub fn not_json() -> Self {
359 Self {
360 error_type: MethodErrorType::NotJson,
361 description: None,
362 limit: None,
363 limit_name: None,
364 }
365 }
366
367 pub fn not_request() -> Self {
371 Self {
372 error_type: MethodErrorType::NotRequest,
373 description: None,
374 limit: None,
375 limit_name: None,
376 }
377 }
378
379 pub fn limit(limit_name: impl Into<String>) -> Self {
389 Self {
390 error_type: MethodErrorType::Limit,
391 description: None,
392 limit: None,
393 limit_name: Some(limit_name.into()),
394 }
395 }
396
397 pub fn unknown_capability_with_detail(uri: impl Into<String>) -> Self {
409 Self {
410 error_type: MethodErrorType::UnknownCapability,
411 description: Some(uri.into()),
412 limit: None,
413 limit_name: None,
414 }
415 }
416
417 pub fn custom(error_type: impl Into<MethodErrorType>) -> Self {
423 Self {
424 error_type: error_type.into(),
425 description: None,
426 limit: None,
427 limit_name: None,
428 }
429 }
430}
431
432#[must_use]
442pub fn json_type_name(v: &serde_json::Value) -> &'static str {
443 match v {
444 serde_json::Value::Null => "null",
445 serde_json::Value::Bool(_) => "boolean",
446 serde_json::Value::Number(_) => "number",
447 serde_json::Value::String(_) => "string",
448 serde_json::Value::Array(_) => "array",
449 serde_json::Value::Object(_) => "object",
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456
457 #[test]
460 fn invalid_arguments_serializes_type_and_description() {
461 let e = JmapError::invalid_arguments("ids field is required");
462 let json = serde_json::to_string(&e).unwrap();
463 assert!(
464 json.contains("\"type\""),
465 "must use 'type' key per RFC 8620"
466 );
467 assert!(json.contains("\"invalidArguments\""));
468 assert!(json.contains("\"description\""));
469 assert!(json.contains("ids field is required"));
470 }
471
472 #[test]
473 fn forbidden_omits_description() {
474 let e = JmapError::forbidden();
475 let json = serde_json::to_string(&e).unwrap();
476 assert!(json.contains("\"forbidden\""));
477 assert!(
478 !json.contains("\"description\""),
479 "None description must be omitted"
480 );
481 }
482
483 #[test]
484 fn account_not_found_type_string() {
485 let e = JmapError::account_not_found();
486 let json = serde_json::to_string(&e).unwrap();
487 assert!(json.contains("\"accountNotFound\""));
488 assert!(!json.contains("\"description\""));
489 }
490
491 #[test]
492 fn account_not_supported_by_method_type_string() {
493 let e = JmapError::account_not_supported_by_method();
494 let json = serde_json::to_string(&e).unwrap();
495 assert!(json.contains("\"accountNotSupportedByMethod\""));
496 }
497
498 #[test]
499 fn account_read_only_type_string() {
500 let e = JmapError::account_read_only();
501 let json = serde_json::to_string(&e).unwrap();
502 assert!(json.contains("\"accountReadOnly\""));
503 }
504
505 #[test]
506 fn server_unavailable_type_string() {
507 let e = JmapError::server_unavailable();
508 let json = serde_json::to_string(&e).unwrap();
509 assert!(json.contains("\"serverUnavailable\""));
510 }
511
512 #[test]
513 fn server_fail_includes_description() {
514 let e = JmapError::server_fail("internal error");
515 let json = serde_json::to_string(&e).unwrap();
516 assert!(json.contains("\"serverFail\""));
517 assert!(json.contains("internal error"));
518 }
519
520 #[test]
521 fn server_partial_fail_type_string() {
522 let e = JmapError::server_partial_fail();
523 let json = serde_json::to_string(&e).unwrap();
524 assert!(json.contains("\"serverPartialFail\""));
525 }
526
527 #[test]
528 fn unknown_method_type_string() {
529 let e = JmapError::unknown_method();
530 let json = serde_json::to_string(&e).unwrap();
531 assert!(json.contains("\"unknownMethod\""));
532 }
533
534 #[test]
535 fn invalid_result_reference_type_string() {
536 let e = JmapError::invalid_result_reference();
537 let json = serde_json::to_string(&e).unwrap();
538 assert!(json.contains("\"invalidResultReference\""));
539 }
540
541 #[test]
542 fn cannot_calculate_changes_type_string() {
543 let e = JmapError::cannot_calculate_changes();
544 let json = serde_json::to_string(&e).unwrap();
545 assert!(json.contains("\"cannotCalculateChanges\""));
546 }
547
548 #[test]
549 fn state_mismatch_type_string() {
550 let e = JmapError::state_mismatch();
551 let json = serde_json::to_string(&e).unwrap();
552 assert!(json.contains("\"stateMismatch\""));
553 }
554
555 #[test]
556 fn too_large_type_string() {
557 let e = JmapError::too_large();
558 let json = serde_json::to_string(&e).unwrap();
559 assert!(json.contains("\"tooLarge\""));
560 }
561
562 #[test]
563 fn request_too_large_type_string() {
564 let e = JmapError::request_too_large();
565 let json = serde_json::to_string(&e).unwrap();
566 assert!(json.contains("\"requestTooLarge\""));
567 assert!(!json.contains("\"description\""));
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 unsupported_filter_type_string() {
579 let e = JmapError::unsupported_filter();
580 let json = serde_json::to_string(&e).unwrap();
581 assert!(json.contains("\"unsupportedFilter\""));
582 }
583
584 #[test]
585 fn anchor_not_found_type_string() {
586 let e = JmapError::anchor_not_found();
587 let json = serde_json::to_string(&e).unwrap();
588 assert!(json.contains("\"anchorNotFound\""));
589 }
590
591 #[test]
592 fn from_account_not_found_type_string() {
593 let e = JmapError::from_account_not_found();
594 let json = serde_json::to_string(&e).unwrap();
595 assert!(json.contains("\"fromAccountNotFound\""));
596 assert!(!json.contains("\"description\""));
597 }
598
599 #[test]
600 fn from_account_not_supported_by_method_type_string() {
601 let e = JmapError::from_account_not_supported_by_method();
602 let json = serde_json::to_string(&e).unwrap();
603 assert!(json.contains("\"fromAccountNotSupportedByMethod\""));
604 assert!(!json.contains("\"description\""));
605 }
606
607 #[test]
608 fn unsupported_sort_type_string() {
609 let e = JmapError::unsupported_sort();
610 let json = serde_json::to_string(&e).unwrap();
611 assert!(json.contains("\"unsupportedSort\""));
612 assert!(!json.contains("\"description\""));
613 }
614
615 #[test]
616 fn too_many_changes_with_limit_serializes_limit_field() {
617 let err = JmapError::too_many_changes_with_limit(100);
618 let v = serde_json::to_value(&err).unwrap();
619 assert_eq!(v["type"], "tooManyChanges");
620 assert_eq!(v["limit"], 100u64);
621 assert!(v.get("description").is_none());
622 }
623
624 #[test]
625 fn not_json_type_string() {
626 let e = JmapError::not_json();
627 let json = serde_json::to_string(&e).unwrap();
628 assert!(json.contains("\"notJSON\""));
629 assert!(!json.contains("\"description\""));
630 }
631
632 #[test]
633 fn not_request_type_string() {
634 let e = JmapError::not_request();
635 let json = serde_json::to_string(&e).unwrap();
636 assert!(json.contains("\"notRequest\""));
637 assert!(!json.contains("\"description\""));
638 }
639
640 #[test]
641 fn limit_carries_limit_name_in_its_own_field() {
642 let e = JmapError::limit("maxCallsInRequest");
646 assert_eq!(e.error_type, MethodErrorType::Limit);
647 assert_eq!(e.limit_name.as_deref(), Some("maxCallsInRequest"));
648 assert_eq!(e.description, None);
649 assert_eq!(
652 serde_json::to_value(&e).unwrap(),
653 serde_json::json!({"type": "limit"})
654 );
655 }
656
657 #[test]
659 fn unknown_capability_with_detail_includes_uri() {
660 let e = JmapError::unknown_capability_with_detail("urn:example:unknown");
661 assert_eq!(e.error_type, MethodErrorType::UnknownCapability);
662 assert_eq!(e.description.as_deref(), Some("urn:example:unknown"));
663 }
664
665 #[test]
666 fn custom_error_type_round_trips() {
667 let e = JmapError::custom("urn:example:customError");
668 assert_eq!(
669 e.error_type,
670 MethodErrorType::Other("urn:example:customError".to_owned())
671 );
672 let json = serde_json::to_string(&e).unwrap();
673 assert!(json.contains("\"urn:example:customError\""));
674 let restored: JmapError = serde_json::from_str(&json).unwrap();
675 assert_eq!(restored, e);
676 }
677
678 #[test]
679 fn round_trip_deserialize() {
680 let original = JmapError::invalid_arguments("test");
682 let json = serde_json::to_string(&original).unwrap();
683 let restored: JmapError = serde_json::from_str(&json).unwrap();
684 assert_eq!(restored.error_type, MethodErrorType::InvalidArguments);
685 assert_eq!(restored.description.as_deref(), Some("test"));
686 }
687
688 #[test]
692 fn method_error_type_all_known_variants_round_trip() {
693 let cases: &[(MethodErrorType, &str)] = &[
694 (MethodErrorType::UnknownCapability, "unknownCapability"),
695 (MethodErrorType::NotJson, "notJSON"),
696 (MethodErrorType::NotRequest, "notRequest"),
697 (MethodErrorType::Limit, "limit"),
698 (MethodErrorType::ServerUnavailable, "serverUnavailable"),
699 (MethodErrorType::ServerFail, "serverFail"),
700 (MethodErrorType::ServerPartialFail, "serverPartialFail"),
701 (MethodErrorType::UnknownMethod, "unknownMethod"),
702 (MethodErrorType::InvalidArguments, "invalidArguments"),
703 (
704 MethodErrorType::InvalidResultReference,
705 "invalidResultReference",
706 ),
707 (MethodErrorType::Forbidden, "forbidden"),
708 (MethodErrorType::AccountNotFound, "accountNotFound"),
709 (
710 MethodErrorType::AccountNotSupportedByMethod,
711 "accountNotSupportedByMethod",
712 ),
713 (MethodErrorType::AccountReadOnly, "accountReadOnly"),
714 (MethodErrorType::RequestTooLarge, "requestTooLarge"),
715 (
716 MethodErrorType::CannotCalculateChanges,
717 "cannotCalculateChanges",
718 ),
719 (MethodErrorType::StateMismatch, "stateMismatch"),
720 (MethodErrorType::FromAccountNotFound, "fromAccountNotFound"),
721 (
722 MethodErrorType::FromAccountNotSupportedByMethod,
723 "fromAccountNotSupportedByMethod",
724 ),
725 (MethodErrorType::AnchorNotFound, "anchorNotFound"),
726 (MethodErrorType::UnsupportedSort, "unsupportedSort"),
727 (MethodErrorType::UnsupportedFilter, "unsupportedFilter"),
728 (MethodErrorType::TooManyChanges, "tooManyChanges"),
729 ];
730 for (variant, wire) in cases {
731 assert_eq!(
732 serde_json::to_value(variant).unwrap(),
733 serde_json::Value::String((*wire).to_owned())
734 );
735 let decoded: MethodErrorType =
736 serde_json::from_value(serde_json::Value::String((*wire).to_owned())).unwrap();
737 assert_eq!(&decoded, variant, "{wire} must decode to its variant");
738 assert_eq!(&MethodErrorType::from(*wire), variant);
739 }
740 let decoded: MethodErrorType = serde_json::from_str(r#""rateLimited""#).unwrap();
742 assert_eq!(decoded, MethodErrorType::Other("rateLimited".to_owned()));
743 assert_eq!(serde_json::to_string(&decoded).unwrap(), r#""rateLimited""#);
744 }
745
746 #[test]
748 fn fixture_response_contains_unknown_method_error() {
749 let raw = include_str!("../tests/fixtures/rfc8620-response.json");
750 let v: serde_json::Value = serde_json::from_str(raw).expect("parse fixture");
751 let inv = &v["methodResponses"][3];
752 assert_eq!(inv[0], "error");
753 assert_eq!(inv[1]["type"], "unknownMethod");
754 assert_eq!(inv[2], "c3");
755 }
756}