1use crate::id::{Id, State};
4use serde::{Deserialize, Serialize};
5use std::collections::BTreeMap;
6
7#[derive(Debug, Clone, PartialEq)]
14pub struct Invocation {
15 pub name: String,
17 pub arguments: serde_json::Value,
19 pub call_id: String,
21}
22
23impl Invocation {
24 pub fn new(
26 name: impl Into<String>,
27 arguments: serde_json::Value,
28 call_id: impl Into<String>,
29 ) -> Self {
30 Self {
31 name: name.into(),
32 arguments,
33 call_id: call_id.into(),
34 }
35 }
36}
37
38impl From<(String, serde_json::Value, String)> for Invocation {
39 fn from((name, arguments, call_id): (String, serde_json::Value, String)) -> Self {
40 Self {
41 name,
42 arguments,
43 call_id,
44 }
45 }
46}
47
48impl From<Invocation> for (String, serde_json::Value, String) {
49 fn from(inv: Invocation) -> Self {
50 (inv.name, inv.arguments, inv.call_id)
51 }
52}
53
54impl Serialize for Invocation {
55 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
56 (&self.name, &self.arguments, &self.call_id).serialize(serializer)
57 }
58}
59
60impl<'de> Deserialize<'de> for Invocation {
61 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
62 <(String, serde_json::Value, String)>::deserialize(deserializer).map(Self::from)
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68#[non_exhaustive]
69pub struct JmapRequest {
70 pub using: Vec<String>,
72 #[serde(rename = "methodCalls")]
74 pub method_calls: Vec<Invocation>,
75 #[serde(rename = "createdIds", skip_serializing_if = "Option::is_none")]
77 pub created_ids: Option<BTreeMap<Id, Id>>,
78 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
83 pub extra: serde_json::Map<String, serde_json::Value>,
84}
85
86#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
88#[non_exhaustive]
89pub struct JmapResponse {
90 #[serde(rename = "methodResponses")]
93 pub method_responses: Vec<Invocation>,
94 #[serde(rename = "sessionState")]
96 pub session_state: State,
97 #[serde(rename = "createdIds", skip_serializing_if = "Option::is_none")]
100 pub created_ids: Option<BTreeMap<Id, Id>>,
101 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
106 pub extra: serde_json::Map<String, serde_json::Value>,
107}
108
109impl JmapRequest {
110 pub fn new(
115 using: Vec<String>,
116 method_calls: Vec<Invocation>,
117 created_ids: Option<BTreeMap<Id, Id>>,
118 ) -> Self {
119 Self {
120 using,
121 method_calls,
122 created_ids,
123 extra: serde_json::Map::new(),
124 }
125 }
126}
127
128impl JmapResponse {
129 pub fn new(
134 method_responses: Vec<Invocation>,
135 session_state: State,
136 created_ids: Option<BTreeMap<Id, Id>>,
137 ) -> Self {
138 Self {
139 method_responses,
140 session_state,
141 created_ids,
142 extra: serde_json::Map::new(),
143 }
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use serde_json::json;
151
152 #[test]
154 fn request_deserializes_from_rfc_fixture() {
155 let raw = include_str!("../tests/fixtures/rfc8620-request.json");
156 let req: JmapRequest = serde_json::from_str(raw).expect("deserialize JmapRequest");
157 assert_eq!(req.using.len(), 2);
158 assert_eq!(req.using[0], "urn:ietf:params:jmap:core");
159 assert_eq!(req.using[1], "urn:ietf:params:jmap:mail");
160 assert_eq!(req.method_calls.len(), 3);
161 assert_eq!(req.method_calls[0].name, "method1");
162 assert_eq!(
163 req.method_calls[0].arguments,
164 json!({"arg1": "arg1data", "arg2": "arg2data"})
165 );
166 assert_eq!(req.method_calls[0].call_id, "c1");
167 assert_eq!(req.method_calls[2].name, "method3");
168 assert_eq!(req.method_calls[2].arguments, json!({}));
170 assert!(req.created_ids.is_none());
171 }
172
173 #[test]
175 fn response_deserializes_from_rfc_fixture() {
176 let raw = include_str!("../tests/fixtures/rfc8620-response.json");
177 let resp: JmapResponse = serde_json::from_str(raw).expect("deserialize JmapResponse");
178 assert_eq!(resp.session_state.as_ref(), "75128aab4b1b");
179 assert_eq!(resp.method_responses.len(), 4);
180 assert_eq!(resp.method_responses[0].name, "method1");
181 assert_eq!(resp.method_responses[3].name, "error");
182 assert_eq!(resp.method_responses[3].call_id, "c3");
183 assert!(resp.created_ids.is_none());
184 }
185
186 #[test]
188 fn request_serializes_camelcase() {
189 let req = JmapRequest {
190 using: vec!["urn:ietf:params:jmap:core".into()],
191 method_calls: vec![],
192 created_ids: None,
193 extra: serde_json::Map::new(),
194 };
195 let j = serde_json::to_string(&req).expect("serialize");
196 assert!(
197 j.contains("\"methodCalls\""),
198 "must use camelCase methodCalls"
199 );
200 assert!(!j.contains("\"method_calls\""), "must not use snake_case");
201 }
202
203 #[test]
205 fn response_serializes_camelcase() {
206 let resp = JmapResponse {
207 method_responses: vec![],
208 session_state: "s-1".into(),
209 created_ids: None,
210 extra: serde_json::Map::new(),
211 };
212 let j = serde_json::to_string(&resp).expect("serialize");
213 assert!(j.contains("\"methodResponses\""));
214 assert!(j.contains("\"sessionState\""));
215 }
216
217 #[test]
219 fn created_ids_absent_when_none() {
220 let resp = JmapResponse {
221 method_responses: vec![],
222 session_state: "s-1".into(),
223 created_ids: None,
224 extra: serde_json::Map::new(),
225 };
226 let j = serde_json::to_string(&resp).expect("serialize");
227 assert!(
228 !j.contains("createdIds"),
229 "createdIds must be absent when None"
230 );
231 }
232
233 #[test]
235 fn created_ids_present_when_some() {
236 let mut ids = std::collections::BTreeMap::new();
237 ids.insert(Id::from("c0"), Id::from("server-1"));
238 let resp = JmapResponse {
239 method_responses: vec![],
240 session_state: "s-1".into(),
241 created_ids: Some(ids),
242 extra: serde_json::Map::new(),
243 };
244 let v = serde_json::to_value(&resp).expect("serialize");
245 assert_eq!(v["createdIds"]["c0"], "server-1");
246 }
247
248 #[test]
250 fn invocation_is_three_element_array() {
251 let inv = Invocation::new("m/get", json!({"accountId": "a1"}), "c0");
252 let j = serde_json::to_string(&inv).expect("serialize");
253 assert_eq!(j, r#"["m/get",{"accountId":"a1"},"c0"]"#);
254 }
255
256 #[test]
259 fn rfc_request_method_calls_round_trip() {
260 let raw = include_str!("../tests/fixtures/rfc8620-request.json");
261 let original: serde_json::Value = serde_json::from_str(raw).expect("parse fixture");
262 let req: JmapRequest = serde_json::from_str(raw).expect("deserialize JmapRequest");
263 let back = serde_json::to_value(&req.method_calls).expect("serialize");
264 assert_eq!(back, original["methodCalls"]);
265 }
266
267 #[test]
270 fn invocation_rejects_wrong_arity() {
271 assert!(serde_json::from_str::<Invocation>(r#"["m/get",{}]"#).is_err());
272 assert!(serde_json::from_str::<Invocation>(r#"["m/get",{},"c0","x"]"#).is_err());
273 assert!(serde_json::from_str::<Invocation>(r#"{"name":"m/get"}"#).is_err());
274 }
275
276 #[test]
277 fn invocation_tuple_conversions() {
278 let inv = Invocation::from(("m/get".to_owned(), json!({}), "c0".to_owned()));
279 assert_eq!(inv.name, "m/get");
280 assert_eq!(inv.arguments, json!({}));
281 assert_eq!(inv.call_id, "c0");
282 let t: (String, serde_json::Value, String) = inv.into();
283 assert_eq!(t, ("m/get".to_owned(), json!({}), "c0".to_owned()));
284 }
285
286 #[test]
296 fn request_preserves_vendor_extras() {
297 let raw = json!({
298 "using": ["urn:ietf:params:jmap:core"],
299 "methodCalls": [["m1", {}, "c0"]],
300 "acmeCorpClientTraceId": "trace-7"
301 });
302 let req: JmapRequest = serde_json::from_value(raw).unwrap();
303 assert_eq!(
304 req.extra
305 .get("acmeCorpClientTraceId")
306 .and_then(|v| v.as_str()),
307 Some("trace-7"),
308 "vendor field must land in extra: {:?}",
309 req.extra
310 );
311 let back = serde_json::to_value(&req).unwrap();
312 assert_eq!(
313 back["acmeCorpClientTraceId"], "trace-7",
314 "vendor field must survive serialize"
315 );
316 }
317
318 #[test]
321 fn response_preserves_vendor_extras() {
322 let raw = json!({
323 "methodResponses": [],
324 "sessionState": "s-1",
325 "acmeCorpServerHostName": "srv-3"
326 });
327 let resp: JmapResponse = serde_json::from_value(raw).unwrap();
328 assert_eq!(
329 resp.extra
330 .get("acmeCorpServerHostName")
331 .and_then(|v| v.as_str()),
332 Some("srv-3")
333 );
334 let back = serde_json::to_value(&resp).unwrap();
335 assert_eq!(back["acmeCorpServerHostName"], "srv-3");
336 }
337
338 #[test]
341 fn empty_extras_omitted_from_wire() {
342 let req = JmapRequest::new(vec!["urn:ietf:params:jmap:core".into()], vec![], None);
343 let v = serde_json::to_value(&req).expect("serialize");
344 let obj = v.as_object().expect("object");
345 assert_eq!(
348 obj.len(),
349 2,
350 "empty extras + skipped optionals must yield exactly using+methodCalls; got {v}"
351 );
352 assert!(obj.contains_key("using"));
353 assert!(obj.contains_key("methodCalls"));
354 }
355
356 #[test]
384 fn request_extra_collision_with_typed_field_round_trip_fails() {
385 let mut req = JmapRequest::new(
386 vec!["urn:ietf:params:jmap:core".into()],
387 vec![Invocation::new("real-method", json!({}), "c-real")],
388 None,
389 );
390 req.extra
393 .insert("methodCalls".into(), json!([["acmeFake", {}, "c-fake"]]));
394
395 let serialised = serde_json::to_string(&req).expect("serialize must succeed");
397 let occurrences = serialised.matches("\"methodCalls\":").count();
398 assert_eq!(
399 occurrences, 2,
400 "wire output must contain two duplicate methodCalls keys; \
401 got {serialised}"
402 );
403
404 let err = serde_json::from_str::<JmapRequest>(&serialised)
408 .expect_err("deserialize must reject duplicate-key wire form");
409 assert!(
410 err.to_string().contains("duplicate field"),
411 "error must mention duplicate field; got: {err}"
412 );
413 }
414
415 #[test]
418 fn created_ids_serialize_in_sorted_key_order() {
419 let mut ids = std::collections::BTreeMap::new();
420 ids.insert(Id::from("c2"), Id::from("server-2"));
421 ids.insert(Id::from("c1"), Id::from("server-1"));
422 let resp = JmapResponse {
423 method_responses: vec![],
424 session_state: "s-1".into(),
425 created_ids: Some(ids),
426 extra: serde_json::Map::new(),
427 };
428 let j = serde_json::to_string(&resp).expect("serialize");
429 assert!(
430 j.contains(r#""createdIds":{"c1":"server-1","c2":"server-2"}"#),
431 "createdIds keys must be sorted: {j}"
432 );
433 }
434}