use crate::Id;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize, Deserialize)]
#[error("{error_type}")]
#[non_exhaustive]
pub struct JmapError {
#[serde(rename = "type")]
pub error_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "existingId", skip_serializing_if = "Option::is_none")]
pub existing_id: Option<Id>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u64>,
}
impl JmapError {
pub fn invalid_arguments(desc: impl Into<String>) -> Self {
Self {
error_type: "invalidArguments".into(),
description: Some(desc.into()),
existing_id: None,
limit: None,
}
}
pub fn forbidden() -> Self {
Self {
error_type: "forbidden".into(),
description: None,
existing_id: None,
limit: None,
}
}
pub fn not_found() -> Self {
Self {
error_type: "notFound".into(),
description: None,
existing_id: None,
limit: None,
}
}
pub fn account_not_found() -> Self {
Self {
error_type: "accountNotFound".into(),
description: None,
existing_id: None,
limit: None,
}
}
pub fn account_not_supported_by_method() -> Self {
Self {
error_type: "accountNotSupportedByMethod".into(),
description: None,
existing_id: None,
limit: None,
}
}
pub fn account_read_only() -> Self {
Self {
error_type: "accountReadOnly".into(),
description: None,
existing_id: None,
limit: None,
}
}
pub fn server_unavailable() -> Self {
Self {
error_type: "serverUnavailable".into(),
description: None,
existing_id: None,
limit: None,
}
}
pub fn server_fail(desc: impl Into<String>) -> Self {
Self {
error_type: "serverFail".into(),
description: Some(desc.into()),
existing_id: None,
limit: None,
}
}
pub fn server_partial_fail() -> Self {
Self {
error_type: "serverPartialFail".into(),
description: None,
existing_id: None,
limit: None,
}
}
pub fn unknown_method() -> Self {
Self {
error_type: "unknownMethod".into(),
description: None,
existing_id: None,
limit: None,
}
}
pub fn invalid_result_reference() -> Self {
Self {
error_type: "invalidResultReference".into(),
description: None,
existing_id: None,
limit: None,
}
}
pub fn cannot_calculate_changes() -> Self {
Self {
error_type: "cannotCalculateChanges".into(),
description: None,
existing_id: None,
limit: None,
}
}
pub fn state_mismatch() -> Self {
Self {
error_type: "stateMismatch".into(),
description: None,
existing_id: None,
limit: None,
}
}
pub fn too_large() -> Self {
Self {
error_type: "tooLarge".into(),
description: None,
existing_id: None,
limit: None,
}
}
pub fn request_too_large() -> Self {
Self {
error_type: "requestTooLarge".into(),
description: None,
existing_id: None,
limit: None,
}
}
pub fn over_quota() -> Self {
Self {
error_type: "overQuota".into(),
description: None,
existing_id: None,
limit: None,
}
}
pub fn rate_limit() -> Self {
Self {
error_type: "rateLimit".into(),
description: None,
existing_id: None,
limit: None,
}
}
pub fn invalid_patch() -> Self {
Self {
error_type: "invalidPatch".into(),
description: None,
existing_id: None,
limit: None,
}
}
pub fn will_destroy() -> Self {
Self {
error_type: "willDestroy".into(),
description: None,
existing_id: None,
limit: None,
}
}
pub fn invalid_properties() -> Self {
Self {
error_type: "invalidProperties".into(),
description: None,
existing_id: None,
limit: None,
}
}
pub fn singleton() -> Self {
Self {
error_type: "singleton".into(),
description: None,
existing_id: None,
limit: None,
}
}
pub fn unsupported_filter() -> Self {
Self {
error_type: "unsupportedFilter".into(),
description: None,
existing_id: None,
limit: None,
}
}
pub fn anchor_not_found() -> Self {
Self {
error_type: "anchorNotFound".into(),
description: None,
existing_id: None,
limit: None,
}
}
pub fn already_exists(existing_id: Id) -> Self {
Self {
error_type: "alreadyExists".into(),
description: None,
existing_id: Some(existing_id),
limit: None,
}
}
pub fn from_account_not_found() -> Self {
Self {
error_type: "fromAccountNotFound".into(),
description: None,
existing_id: None,
limit: None,
}
}
pub fn from_account_not_supported_by_method() -> Self {
Self {
error_type: "fromAccountNotSupportedByMethod".into(),
description: None,
existing_id: None,
limit: None,
}
}
pub fn unsupported_sort() -> Self {
Self {
error_type: "unsupportedSort".into(),
description: None,
existing_id: None,
limit: None,
}
}
#[deprecated(
note = "always use too_many_changes_with_limit to include the limit per RFC 8620 §9.6.1"
)]
pub fn too_many_changes() -> Self {
Self {
error_type: "tooManyChanges".into(),
description: None,
existing_id: None,
limit: None,
}
}
pub fn too_many_changes_with_limit(limit: u64) -> Self {
Self {
error_type: "tooManyChanges".into(),
description: None,
existing_id: None,
limit: Some(limit),
}
}
pub fn not_json() -> Self {
Self {
error_type: "notJSON".into(),
description: None,
existing_id: None,
limit: None,
}
}
pub fn not_request() -> Self {
Self {
error_type: "notRequest".into(),
description: None,
existing_id: None,
limit: None,
}
}
pub fn limit(limit_name: impl Into<String>) -> Self {
Self {
error_type: "limit".into(),
description: Some(limit_name.into()),
existing_id: None,
limit: None,
}
}
#[deprecated(
note = "always use unknown_capability_with_detail to include the URI in the error"
)]
pub fn unknown_capability() -> Self {
Self {
error_type: "unknownCapability".into(),
description: None,
existing_id: None,
limit: None,
}
}
pub fn unknown_capability_with_detail(uri: impl Into<String>) -> Self {
Self {
error_type: "unknownCapability".into(),
description: Some(uri.into()),
existing_id: None,
limit: None,
}
}
pub fn custom(error_type: impl Into<String>) -> Self {
Self {
error_type: error_type.into(),
description: None,
existing_id: None,
limit: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn invalid_arguments_serializes_type_and_description() {
let e = JmapError::invalid_arguments("ids field is required");
let json = serde_json::to_string(&e).unwrap();
assert!(
json.contains("\"type\""),
"must use 'type' key per RFC 8620"
);
assert!(json.contains("\"invalidArguments\""));
assert!(json.contains("\"description\""));
assert!(json.contains("ids field is required"));
}
#[test]
fn forbidden_omits_description() {
let e = JmapError::forbidden();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"forbidden\""));
assert!(
!json.contains("\"description\""),
"None description must be omitted"
);
}
#[test]
fn not_found_type_string() {
let e = JmapError::not_found();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"notFound\""));
assert!(!json.contains("\"description\""));
}
#[test]
fn account_not_found_type_string() {
let e = JmapError::account_not_found();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"accountNotFound\""));
assert!(!json.contains("\"description\""));
}
#[test]
fn account_not_supported_by_method_type_string() {
let e = JmapError::account_not_supported_by_method();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"accountNotSupportedByMethod\""));
}
#[test]
fn account_read_only_type_string() {
let e = JmapError::account_read_only();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"accountReadOnly\""));
}
#[test]
fn server_unavailable_type_string() {
let e = JmapError::server_unavailable();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"serverUnavailable\""));
}
#[test]
fn server_fail_includes_description() {
let e = JmapError::server_fail("internal error");
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"serverFail\""));
assert!(json.contains("internal error"));
}
#[test]
fn server_partial_fail_type_string() {
let e = JmapError::server_partial_fail();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"serverPartialFail\""));
}
#[test]
fn unknown_method_type_string() {
let e = JmapError::unknown_method();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"unknownMethod\""));
}
#[test]
fn invalid_result_reference_type_string() {
let e = JmapError::invalid_result_reference();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"invalidResultReference\""));
}
#[test]
fn cannot_calculate_changes_type_string() {
let e = JmapError::cannot_calculate_changes();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"cannotCalculateChanges\""));
}
#[test]
fn state_mismatch_type_string() {
let e = JmapError::state_mismatch();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"stateMismatch\""));
}
#[test]
fn too_large_type_string() {
let e = JmapError::too_large();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"tooLarge\""));
}
#[test]
fn request_too_large_type_string() {
let e = JmapError::request_too_large();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"requestTooLarge\""));
assert!(!json.contains("\"description\""));
}
#[test]
fn over_quota_type_string() {
let e = JmapError::over_quota();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"overQuota\""));
}
#[test]
fn rate_limit_type_string() {
let e = JmapError::rate_limit();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"rateLimit\""));
}
#[test]
fn invalid_patch_type_string() {
let e = JmapError::invalid_patch();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"invalidPatch\""));
}
#[test]
fn will_destroy_type_string() {
let e = JmapError::will_destroy();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"willDestroy\""));
}
#[test]
fn invalid_properties_type_string() {
let e = JmapError::invalid_properties();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"invalidProperties\""));
}
#[test]
fn singleton_type_string() {
let e = JmapError::singleton();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"singleton\""));
}
#[test]
fn unsupported_filter_type_string() {
let e = JmapError::unsupported_filter();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"unsupportedFilter\""));
}
#[test]
fn anchor_not_found_type_string() {
let e = JmapError::anchor_not_found();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"anchorNotFound\""));
}
#[test]
fn already_exists_includes_existing_id() {
let e = JmapError::already_exists(Id::from("abc123"));
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"alreadyExists\""));
assert!(json.contains("\"existingId\""));
assert!(json.contains("\"abc123\""));
assert!(!json.contains("\"description\""));
}
#[test]
fn from_account_not_found_type_string() {
let e = JmapError::from_account_not_found();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"fromAccountNotFound\""));
assert!(!json.contains("\"description\""));
}
#[test]
fn from_account_not_supported_by_method_type_string() {
let e = JmapError::from_account_not_supported_by_method();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"fromAccountNotSupportedByMethod\""));
assert!(!json.contains("\"description\""));
}
#[test]
fn unsupported_sort_type_string() {
let e = JmapError::unsupported_sort();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"unsupportedSort\""));
assert!(!json.contains("\"description\""));
}
#[allow(deprecated)]
#[test]
fn too_many_changes_type_string() {
let e = JmapError::too_many_changes();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"tooManyChanges\""));
assert!(!json.contains("\"description\""));
}
#[test]
fn too_many_changes_with_limit_serializes_limit_field() {
let err = JmapError::too_many_changes_with_limit(100);
let v = serde_json::to_value(&err).unwrap();
assert_eq!(v["type"], "tooManyChanges");
assert_eq!(v["limit"], 100u64);
assert!(v.get("description").is_none());
}
#[allow(deprecated)]
#[test]
fn too_many_changes_without_limit_has_no_limit_field() {
let err = JmapError::too_many_changes();
let v = serde_json::to_value(&err).unwrap();
assert_eq!(v["type"], "tooManyChanges");
assert!(v.get("limit").is_none());
}
#[test]
fn not_json_type_string() {
let e = JmapError::not_json();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"notJSON\""));
assert!(!json.contains("\"description\""));
}
#[test]
fn not_request_type_string() {
let e = JmapError::not_request();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"notRequest\""));
assert!(!json.contains("\"description\""));
}
#[test]
fn limit_includes_limit_name_in_description() {
let e = JmapError::limit("maxCallsInRequest");
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"limit\""));
assert!(json.contains("\"maxCallsInRequest\""));
}
#[allow(deprecated)]
#[test]
fn unknown_capability_type_string() {
let e = JmapError::unknown_capability();
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"unknownCapability\""));
assert!(!json.contains("\"description\""));
}
#[test]
fn unknown_capability_with_detail_includes_uri() {
let e = JmapError::unknown_capability_with_detail("urn:example:unknown");
assert_eq!(e.error_type, "unknownCapability");
assert_eq!(e.description.as_deref(), Some("urn:example:unknown"));
}
#[test]
fn custom_error_type_round_trips() {
let e = JmapError::custom("urn:example:customError");
assert_eq!(e.error_type, "urn:example:customError");
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("\"urn:example:customError\""));
let restored: JmapError = serde_json::from_str(&json).unwrap();
assert_eq!(restored.error_type, "urn:example:customError");
}
#[test]
fn round_trip_deserialize() {
let original = JmapError::invalid_arguments("test");
let json = serde_json::to_string(&original).unwrap();
let restored: JmapError = serde_json::from_str(&json).unwrap();
assert_eq!(restored.error_type, "invalidArguments");
assert_eq!(restored.description.as_deref(), Some("test"));
}
#[test]
fn fixture_response_contains_unknown_method_error() {
let raw = include_str!("../tests/fixtures/rfc8620-response.json");
let v: serde_json::Value = serde_json::from_str(raw).expect("parse fixture");
let inv = &v["methodResponses"][3];
assert_eq!(inv[0], "error");
assert_eq!(inv[1]["type"], "unknownMethod");
assert_eq!(inv[2], "c3");
}
}