Skip to main content

jmap_base_client/
query.rs

1//! [`QueryParams`]: the standard `/query` window arguments (RFC 8620 §5.5).
2
3use jmap_types::Id;
4
5/// The RFC 8620 §5.5 `/query` arguments shared by every `Foo/query`
6/// method: which window of the result list to return and whether to
7/// compute the total.
8///
9/// `accountId`, `filter` and `sort` are passed to each `*_query` method
10/// separately because their types differ per object type; the
11/// type-specific arguments some methods define (for example
12/// `collapseThreads` on `Email/query`, RFC 8621 §4.4) are passed the
13/// same way. Every field defaults to `None`, which omits the argument so
14/// the server applies the RFC 8620 §5.5 default:
15///
16/// ```
17/// use jmap_base_client::QueryParams;
18///
19/// let page = QueryParams {
20///     limit: Some(50),
21///     calculate_total: Some(true),
22///     ..QueryParams::default()
23/// };
24/// # let _ = page;
25/// ```
26#[derive(Debug, Default, Clone, PartialEq)]
27pub struct QueryParams {
28    /// `position` (RFC 8620 §5.5, `Int`, default `0`): zero-based index of
29    /// the first id to return. A negative value is an offset from the end
30    /// of the result list. Ignored by the server when [`Self::anchor`] is
31    /// set.
32    pub position: Option<i64>,
33    /// `anchor` (RFC 8620 §5.5, `Id|null`): an id in the result list; the
34    /// returned window starts at its index plus [`Self::anchor_offset`].
35    /// The server returns `anchorNotFound` if the id is not in the
36    /// results.
37    pub anchor: Option<Id>,
38    /// `anchorOffset` (RFC 8620 §5.5, `Int`, default `0`): index of the
39    /// first result relative to [`Self::anchor`]; may be negative.
40    pub anchor_offset: Option<i64>,
41    /// `limit` (RFC 8620 §5.5, `UnsignedInt|null`): maximum number of ids
42    /// to return. The server may clamp it to its own maximum and report
43    /// the clamped value in the response.
44    pub limit: Option<u64>,
45    /// `calculateTotal` (RFC 8620 §5.5, `Boolean`, default `false`): ask
46    /// the server to return `total` in the response.
47    pub calculate_total: Option<bool>,
48    /// Vendor or extension arguments not covered by the typed fields.
49    ///
50    /// Typed fields and the method's own `accountId` / `filter` / `sort`
51    /// and type-specific arguments win on a key collision; place vendor
52    /// extensions under vendor-prefixed keys (e.g. `"acmeCorpFoo"`).
53    pub extra: serde_json::Map<String, serde_json::Value>,
54}
55
56impl QueryParams {
57    /// Build the `/query` arguments object: [`Self::extra`], then
58    /// `accountId` and every typed field that is `Some`.
59    ///
60    /// Callers add `filter`, `sort` and type-specific arguments to the
61    /// returned object.
62    pub fn to_arguments(&self, account_id: &str) -> serde_json::Value {
63        let mut args = self.extra.clone();
64        args.insert("accountId".into(), account_id.into());
65        if let Some(p) = self.position {
66            args.insert("position".into(), p.into());
67        }
68        if let Some(a) = &self.anchor {
69            args.insert("anchor".into(), a.as_ref().into());
70        }
71        if let Some(o) = self.anchor_offset {
72            args.insert("anchorOffset".into(), o.into());
73        }
74        if let Some(l) = self.limit {
75            args.insert("limit".into(), l.into());
76        }
77        if let Some(c) = self.calculate_total {
78            args.insert("calculateTotal".into(), c.into());
79        }
80        serde_json::Value::Object(args)
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use serde_json::json;
88
89    /// Oracle: RFC 8620 §5.5 argument names, hand-written JSON.
90    #[test]
91    fn all_fields_use_rfc8620_wire_names() {
92        let params = QueryParams {
93            position: Some(-5),
94            anchor: Some(Id::from("m1")),
95            anchor_offset: Some(-1),
96            limit: Some(20),
97            calculate_total: Some(true),
98            extra: serde_json::Map::new(),
99        };
100        assert_eq!(
101            params.to_arguments("acc1"),
102            json!({
103                "accountId": "acc1",
104                "position": -5,
105                "anchor": "m1",
106                "anchorOffset": -1,
107                "limit": 20,
108                "calculateTotal": true,
109            })
110        );
111    }
112
113    /// Oracle: RFC 8620 §5.5 — every window argument has a default, so an
114    /// all-`None` value sends only `accountId`.
115    #[test]
116    fn default_sends_only_account_id() {
117        assert_eq!(
118            QueryParams::default().to_arguments("acc1"),
119            json!({ "accountId": "acc1" })
120        );
121    }
122
123    /// Oracle: hand-written JSON — vendor keys pass through; a colliding
124    /// key loses to the typed field and to `accountId`.
125    #[test]
126    fn extra_passes_through_and_typed_fields_win() {
127        let mut extra = serde_json::Map::new();
128        extra.insert("acmeCorpFoo".into(), json!(1));
129        extra.insert("limit".into(), json!(999));
130        extra.insert("accountId".into(), json!("evil"));
131        let params = QueryParams {
132            limit: Some(10),
133            extra,
134            ..QueryParams::default()
135        };
136        assert_eq!(
137            params.to_arguments("acc1"),
138            json!({ "accountId": "acc1", "limit": 10, "acmeCorpFoo": 1 })
139        );
140    }
141}