Skip to main content

io_jmap/rfc8620/
types.rs

1//! JMAP Core (RFC 8620) data types: capability URN, session, method errors,
2//! request/response shape, filter combinator, result references.
3
4use core::{error::Error, fmt};
5
6use alloc::{collections::BTreeMap, format, string::String, vec::Vec};
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use url::Url;
11
12/// The JMAP session object returned by the well-known URL (RFC 8620 §2).
13#[derive(Clone, Debug, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct JmapSession {
16    pub username: String,
17    pub accounts: BTreeMap<String, JmapAccountInfo>,
18    pub primary_accounts: BTreeMap<String, String>,
19    pub capabilities: BTreeMap<String, Value>,
20    pub api_url: Url,
21    pub download_url: String,
22    pub upload_url: String,
23    pub event_source_url: String,
24    pub state: String,
25}
26
27impl JmapSession {
28    /// Returns the primary account ID for the given capability URN, or an empty
29    /// string if none is advertised.
30    pub fn primary_account_id_for(&self, capability: &str) -> String {
31        self.primary_accounts
32            .get(capability)
33            .cloned()
34            .unwrap_or_default()
35    }
36}
37
38/// Information about a single JMAP account within a session.
39#[derive(Clone, Debug, Serialize, Deserialize)]
40#[serde(rename_all = "camelCase")]
41pub struct JmapAccountInfo {
42    pub name: String,
43    pub is_personal: bool,
44    pub is_read_only: bool,
45    pub account_capabilities: BTreeMap<String, Value>,
46}
47
48/// A JMAP method-level error (RFC 8620 §3.6.1).
49#[derive(Clone, Debug, Serialize, Deserialize)]
50#[serde(tag = "type", rename_all = "camelCase")]
51pub enum JmapMethodError {
52    ServerFail {
53        description: Option<String>,
54    },
55    ServerPartialFail,
56    ServerUnavailable {
57        description: Option<String>,
58    },
59    UnknownCapability {
60        description: Option<String>,
61    },
62    NotJson {
63        description: Option<String>,
64    },
65    NotRequest {
66        description: Option<String>,
67    },
68    Limit {
69        description: Option<String>,
70        limit: String,
71    },
72    InvalidArguments {
73        description: Option<String>,
74    },
75    RequestTooLarge,
76    NotFound,
77    InvalidPatch {
78        description: Option<String>,
79    },
80    WillDestroy {
81        description: Option<String>,
82    },
83    InvalidProperties {
84        description: Option<String>,
85        #[serde(default)]
86        properties: Vec<String>,
87    },
88    Singleton,
89    UnknownMethod {
90        description: Option<String>,
91    },
92    /// Server can no longer compute changes from `sinceState` (RFC 8620 §5.2):
93    /// callers MUST fall back to `Foo/get` and resume from the returned state.
94    CannotCalculateChanges {
95        description: Option<String>,
96    },
97    #[serde(other)]
98    Unknown,
99}
100
101impl fmt::Display for JmapMethodError {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        match self {
104            Self::ServerFail { description } => {
105                write!(f, "JMAP serverFail")?;
106                if let Some(d) = description {
107                    write!(f, ": {d}")?;
108                }
109                Ok(())
110            }
111            Self::ServerPartialFail => write!(f, "JMAP serverPartialFail"),
112            Self::ServerUnavailable { description } => {
113                write!(f, "JMAP serverUnavailable")?;
114                if let Some(d) = description {
115                    write!(f, ": {d}")?;
116                }
117                Ok(())
118            }
119            Self::UnknownCapability { description } => {
120                write!(f, "JMAP unknownCapability")?;
121                if let Some(d) = description {
122                    write!(f, ": {d}")?;
123                }
124                Ok(())
125            }
126            Self::NotJson { description } => {
127                write!(f, "JMAP notJson")?;
128                if let Some(d) = description {
129                    write!(f, ": {d}")?;
130                }
131                Ok(())
132            }
133            Self::NotRequest { description } => {
134                write!(f, "JMAP notRequest")?;
135                if let Some(d) = description {
136                    write!(f, ": {d}")?;
137                }
138                Ok(())
139            }
140            Self::Limit { description, limit } => {
141                write!(f, "JMAP limit ({limit})")?;
142                if let Some(d) = description {
143                    write!(f, ": {d}")?;
144                }
145                Ok(())
146            }
147            Self::InvalidArguments { description } => {
148                write!(f, "JMAP invalidArguments")?;
149                if let Some(d) = description {
150                    write!(f, ": {d}")?;
151                }
152                Ok(())
153            }
154            Self::RequestTooLarge => write!(f, "JMAP requestTooLarge"),
155            Self::NotFound => write!(f, "JMAP notFound"),
156            Self::InvalidPatch { description } => {
157                write!(f, "JMAP invalidPatch")?;
158                if let Some(d) = description {
159                    write!(f, ": {d}")?;
160                }
161                Ok(())
162            }
163            Self::WillDestroy { description } => {
164                write!(f, "JMAP willDestroy")?;
165                if let Some(d) = description {
166                    write!(f, ": {d}")?;
167                }
168                Ok(())
169            }
170            Self::InvalidProperties {
171                description,
172                properties,
173            } => {
174                write!(f, "JMAP invalidProperties")?;
175                if !properties.is_empty() {
176                    write!(f, " [{}]", properties.join(", "))?;
177                }
178                if let Some(d) = description {
179                    write!(f, ": {d}")?;
180                }
181                Ok(())
182            }
183            Self::Singleton => write!(f, "JMAP singleton"),
184            Self::UnknownMethod { description } => {
185                write!(f, "JMAP unknownMethod")?;
186                if let Some(d) = description {
187                    write!(f, ": {d}")?;
188                }
189                Ok(())
190            }
191            Self::CannotCalculateChanges { description } => {
192                write!(f, "JMAP cannotCalculateChanges")?;
193                if let Some(d) = description {
194                    write!(f, ": {d}")?;
195                }
196                Ok(())
197            }
198            Self::Unknown => write!(f, "JMAP unknown error"),
199        }
200    }
201}
202
203impl Error for JmapMethodError {}
204
205/// Per-object error returned in `Foo/set` responses (RFC 8620 §5.3).
206#[derive(Clone, Debug, Deserialize)]
207#[serde(rename_all = "camelCase")]
208pub struct JmapSetError {
209    pub r#type: String,
210    pub description: Option<String>,
211    #[serde(default)]
212    pub properties: Vec<String>,
213}
214
215/// A JMAP filter (RFC 8620 §5.5): a protocol-specific condition (e.g.
216/// [`crate::rfc8621::email::JmapEmailFilter`]) or a logical combinator.
217///
218/// `untagged` serde: presence of `operator` picks [`JmapFilterOperator`].
219#[derive(Clone, Debug, Serialize, Deserialize)]
220#[serde(untagged)]
221pub enum JmapFilter<C> {
222    Operator(JmapFilterOperator<C>),
223    Condition(C),
224}
225
226impl<C> From<C> for JmapFilter<C> {
227    fn from(condition: C) -> Self {
228        JmapFilter::Condition(condition)
229    }
230}
231
232impl<C> JmapFilter<C> {
233    /// Wraps `conditions` in an AND combinator.
234    pub fn and(conditions: Vec<JmapFilter<C>>) -> Self {
235        JmapFilter::Operator(JmapFilterOperator {
236            operator: JmapFilterOperatorKind::And,
237            conditions,
238        })
239    }
240
241    /// Wraps `conditions` in an OR combinator.
242    pub fn or(conditions: Vec<JmapFilter<C>>) -> Self {
243        JmapFilter::Operator(JmapFilterOperator {
244            operator: JmapFilterOperatorKind::Or,
245            conditions,
246        })
247    }
248
249    /// Wraps `conditions` in a NOT combinator. Satisfied when no condition
250    /// matches (RFC 8620 §5.5), so a single-element vec models unary negation.
251    pub fn not(conditions: Vec<JmapFilter<C>>) -> Self {
252        JmapFilter::Operator(JmapFilterOperator {
253            operator: JmapFilterOperatorKind::Not,
254            conditions,
255        })
256    }
257}
258
259/// Logical combinator over a list of sub-filters.
260#[derive(Clone, Debug, Serialize, Deserialize)]
261#[serde(rename_all = "camelCase")]
262pub struct JmapFilterOperator<C> {
263    pub operator: JmapFilterOperatorKind,
264    pub conditions: Vec<JmapFilter<C>>,
265}
266
267/// AND / OR / NOT, serialized as RFC 8620 §5.5 spells them.
268#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
269#[serde(rename_all = "UPPERCASE")]
270pub enum JmapFilterOperatorKind {
271    And,
272    Or,
273    Not,
274}
275
276/// A JMAP result reference (RFC 8620 §7.1) used to back-reference an
277/// earlier method call's result within a batch request.
278#[derive(Serialize)]
279#[serde(rename_all = "camelCase")]
280pub struct JmapResultReference<'a> {
281    pub result_of: &'a str,
282    pub name: &'static str,
283    pub path: &'static str,
284}
285
286/// The JMAP Request object (RFC 8620 §3.3).
287#[derive(Clone, Debug, Serialize)]
288#[serde(rename_all = "camelCase")]
289pub struct JmapRequest {
290    /// Capability URNs required by the methods in this request.
291    pub using: Vec<String>,
292
293    /// The method calls to execute, as `(methodName, args, callId)`
294    /// tuples.
295    pub method_calls: Vec<(String, Value, String)>,
296
297    /// Client-assigned IDs for newly created objects.
298    #[serde(skip_serializing_if = "Option::is_none")]
299    pub created_ids: Option<BTreeMap<String, String>>,
300}
301
302/// The JMAP Response object (RFC 8620 §3.4).
303#[derive(Clone, Debug, Deserialize)]
304#[serde(rename_all = "camelCase")]
305pub struct JmapResponse {
306    /// Method responses in `(methodName, result, callId)` format.
307    ///
308    /// If a method failed, `methodName` is `"error"` and `result` is a
309    /// [`JmapMethodError`] object.
310    pub method_responses: Vec<(String, Value, String)>,
311
312    /// Server-assigned IDs for objects created by this request.
313    #[serde(default)]
314    pub created_ids: Option<BTreeMap<String, String>>,
315
316    /// The current state of the session after this request.
317    pub session_state: String,
318}
319
320/// Builder for batched JMAP requests: multiple method calls in one HTTP
321/// request, with generated call IDs for [`JmapResultReference`] back-refs.
322#[derive(Debug, Default)]
323pub struct JmapBatch {
324    calls: Vec<(String, Value, String)>,
325    counter: usize,
326}
327
328impl JmapBatch {
329    /// Creates a new empty batch.
330    pub fn new() -> Self {
331        Self::default()
332    }
333
334    /// Adds a method call. Returns the call ID (`"c0"`, `"c1"`, …) for use in
335    /// back-references from later calls.
336    pub fn add(&mut self, method: impl Into<String>, args: Value) -> String {
337        let call_id = format!("c{}", self.counter);
338        self.counter += 1;
339        self.calls.push((method.into(), args, call_id.clone()));
340        call_id
341    }
342
343    /// Consumes the batch and returns a [`JmapRequest`].
344    pub fn into_request(self, using: Vec<String>) -> JmapRequest {
345        JmapRequest {
346            using,
347            method_calls: self.calls,
348            created_ids: None,
349        }
350    }
351}
352
353/// A single added item in a `Foo/queryChanges` response (RFC 8620 §5.6).
354#[derive(Clone, Debug, Deserialize)]
355pub struct JmapAddedItem {
356    pub id: String,
357    pub index: u64,
358}
359
360#[cfg(test)]
361mod tests {
362    use alloc::vec;
363
364    use serde_json::json;
365
366    use super::*;
367
368    #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
369    struct Cond {
370        from: Option<String>,
371    }
372
373    #[test]
374    fn condition_serializes_flat() {
375        let f: JmapFilter<Cond> = JmapFilter::Condition(Cond {
376            from: Some("alice".into()),
377        });
378        assert_eq!(
379            serde_json::to_value(&f).unwrap(),
380            json!({ "from": "alice" })
381        );
382    }
383
384    #[test]
385    fn and_serializes_with_operator_key() {
386        let f: JmapFilter<Cond> = JmapFilter::and(vec![
387            JmapFilter::Condition(Cond {
388                from: Some("a".into()),
389            }),
390            JmapFilter::Condition(Cond {
391                from: Some("b".into()),
392            }),
393        ]);
394        assert_eq!(
395            serde_json::to_value(&f).unwrap(),
396            json!({
397                "operator": "AND",
398                "conditions": [
399                    { "from": "a" },
400                    { "from": "b" },
401                ],
402            }),
403        );
404    }
405
406    #[test]
407    fn not_wraps_a_single_subfilter() {
408        let f: JmapFilter<Cond> = JmapFilter::not(vec![JmapFilter::Condition(Cond {
409            from: Some("a".into()),
410        })]);
411        assert_eq!(
412            serde_json::to_value(&f).unwrap(),
413            json!({
414                "operator": "NOT",
415                "conditions": [{ "from": "a" }],
416            }),
417        );
418    }
419
420    #[test]
421    fn deserialize_discriminates_on_operator_key() {
422        let v = json!({ "from": "alice" });
423        let f: JmapFilter<Cond> = serde_json::from_value(v).unwrap();
424        assert!(matches!(f, JmapFilter::Condition(Cond { from: Some(_) })));
425
426        let v = json!({
427            "operator": "OR",
428            "conditions": [{ "from": "a" }, { "from": "b" }],
429        });
430        let f: JmapFilter<Cond> = serde_json::from_value(v).unwrap();
431        assert!(matches!(
432            f,
433            JmapFilter::Operator(JmapFilterOperator {
434                operator: JmapFilterOperatorKind::Or,
435                ..
436            })
437        ));
438    }
439}