Skip to main content

mailrs_jmap/
error.rs

1//! JMAP standard method errors (RFC 8620 §3.6.2).
2//!
3//! Each variant serialises into the `{"type": "...", "description": "..."}`
4//! shape JMAP clients expect. The dispatcher converts a handler's
5//! `Result<Value, JmapMethodError>` into that shape automatically.
6
7use serde_json::Value;
8
9/// Subset of standard JMAP method errors used by this crate's handlers.
10///
11/// JMAP defines more (e.g. `cannotCalculateChanges`, `tooLarge`) — feel free
12/// to extend; the `Other` variant is the escape hatch for anything not yet
13/// modelled.
14#[derive(Debug, Clone)]
15pub enum JmapMethodError {
16    /// RFC 8620 §3.6.2 — `serverFail` with an opaque description.
17    ServerFail(String),
18    /// RFC 8620 §3.6.2 — store is configured but currently unreachable.
19    ServerUnavailable(String),
20    /// RFC 8620 §3.6.2 — required arg missing or shape wrong.
21    InvalidArguments(String),
22    /// RFC 8620 §3.6.2 — method name not recognised.
23    UnknownMethod(String),
24    /// RFC 8620 §3.6.2 — caller not authorised for this account.
25    AccountNotFound,
26    /// Escape hatch for any error type not modelled by a dedicated variant
27    /// above. `kind` becomes the JMAP `"type"` value verbatim; `description`
28    /// becomes the human-readable `"description"`.
29    Other {
30        /// The JMAP error `"type"` token (e.g. `"tooLarge"`, `"cannotCalculateChanges"`).
31        kind: String,
32        /// Human-readable description that will be rendered into the `"description"` field.
33        description: String,
34    },
35}
36
37impl JmapMethodError {
38    /// Render to the canonical JMAP error JSON shape.
39    pub fn to_json(&self) -> Value {
40        let (kind, description) = match self {
41            JmapMethodError::ServerFail(d) => ("serverFail", d.as_str()),
42            JmapMethodError::ServerUnavailable(d) => ("serverUnavailable", d.as_str()),
43            JmapMethodError::InvalidArguments(d) => ("invalidArguments", d.as_str()),
44            JmapMethodError::UnknownMethod(d) => ("unknownMethod", d.as_str()),
45            JmapMethodError::AccountNotFound => ("accountNotFound", ""),
46            JmapMethodError::Other { kind, description } => (kind.as_str(), description.as_str()),
47        };
48        if description.is_empty() {
49            serde_json::json!({ "type": kind })
50        } else {
51            serde_json::json!({ "type": kind, "description": description })
52        }
53    }
54}
55
56impl std::fmt::Display for JmapMethodError {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        match self {
59            JmapMethodError::ServerFail(d) => write!(f, "serverFail: {d}"),
60            JmapMethodError::ServerUnavailable(d) => write!(f, "serverUnavailable: {d}"),
61            JmapMethodError::InvalidArguments(d) => write!(f, "invalidArguments: {d}"),
62            JmapMethodError::UnknownMethod(d) => write!(f, "unknownMethod: {d}"),
63            JmapMethodError::AccountNotFound => write!(f, "accountNotFound"),
64            JmapMethodError::Other { kind, description } => write!(f, "{kind}: {description}"),
65        }
66    }
67}
68
69impl std::error::Error for JmapMethodError {}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn server_fail_serialises_with_description() {
77        let err = JmapMethodError::ServerFail("boom".into());
78        let v = err.to_json();
79        assert_eq!(v["type"], "serverFail");
80        assert_eq!(v["description"], "boom");
81    }
82
83    #[test]
84    fn account_not_found_has_no_description() {
85        let err = JmapMethodError::AccountNotFound;
86        let v = err.to_json();
87        assert_eq!(v["type"], "accountNotFound");
88        assert!(v.get("description").is_none());
89    }
90
91    #[test]
92    fn other_variant_uses_kind_and_description() {
93        let err = JmapMethodError::Other {
94            kind: "tooLarge".into(),
95            description: "1 MB > 100 KB".into(),
96        };
97        let v = err.to_json();
98        assert_eq!(v["type"], "tooLarge");
99        assert_eq!(v["description"], "1 MB > 100 KB");
100    }
101}