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}