1use crate::id::{Id, State};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7pub type Invocation = (String, serde_json::Value, String);
11
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14#[non_exhaustive]
15pub struct JmapRequest {
16 pub using: Vec<String>,
18 #[serde(rename = "methodCalls")]
20 pub method_calls: Vec<Invocation>,
21 #[serde(rename = "createdIds", skip_serializing_if = "Option::is_none")]
23 pub created_ids: Option<HashMap<Id, Id>>,
24 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
29 pub extra: serde_json::Map<String, serde_json::Value>,
30}
31
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
34#[non_exhaustive]
35pub struct JmapResponse {
36 #[serde(rename = "methodResponses")]
38 pub method_responses: Vec<Invocation>,
39 #[serde(rename = "sessionState")]
41 pub session_state: State,
42 #[serde(rename = "createdIds", skip_serializing_if = "Option::is_none")]
45 pub created_ids: Option<HashMap<Id, Id>>,
46 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
51 pub extra: serde_json::Map<String, serde_json::Value>,
52}
53
54impl JmapRequest {
55 pub fn new(
60 using: Vec<String>,
61 method_calls: Vec<Invocation>,
62 created_ids: Option<HashMap<Id, Id>>,
63 ) -> Self {
64 Self {
65 using,
66 method_calls,
67 created_ids,
68 extra: serde_json::Map::new(),
69 }
70 }
71}
72
73impl JmapResponse {
74 pub fn new(
79 method_responses: Vec<Invocation>,
80 session_state: State,
81 created_ids: Option<HashMap<Id, Id>>,
82 ) -> Self {
83 Self {
84 method_responses,
85 session_state,
86 created_ids,
87 extra: serde_json::Map::new(),
88 }
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use serde_json::json;
96
97 #[test]
99 fn request_deserializes_from_rfc_fixture() {
100 let raw = include_str!("../tests/fixtures/rfc8620-request.json");
101 let req: JmapRequest = serde_json::from_str(raw).expect("deserialize JmapRequest");
102 assert_eq!(req.using.len(), 2);
103 assert_eq!(req.using[0], "urn:ietf:params:jmap:core");
104 assert_eq!(req.using[1], "urn:ietf:params:jmap:mail");
105 assert_eq!(req.method_calls.len(), 3);
106 assert_eq!(req.method_calls[0].0, "method1");
107 assert_eq!(req.method_calls[0].2, "c1");
108 assert_eq!(req.method_calls[2].0, "method3");
109 assert_eq!(req.method_calls[2].1, json!({}));
111 assert!(req.created_ids.is_none());
112 }
113
114 #[test]
116 fn response_deserializes_from_rfc_fixture() {
117 let raw = include_str!("../tests/fixtures/rfc8620-response.json");
118 let resp: JmapResponse = serde_json::from_str(raw).expect("deserialize JmapResponse");
119 assert_eq!(resp.session_state.as_ref(), "75128aab4b1b");
120 assert_eq!(resp.method_responses.len(), 4);
121 assert_eq!(resp.method_responses[0].0, "method1");
122 assert_eq!(resp.method_responses[3].0, "error");
123 assert_eq!(resp.method_responses[3].2, "c3");
124 assert!(resp.created_ids.is_none());
125 }
126
127 #[test]
129 fn request_serializes_camelcase() {
130 let req = JmapRequest {
131 using: vec!["urn:ietf:params:jmap:core".into()],
132 method_calls: vec![],
133 created_ids: None,
134 extra: serde_json::Map::new(),
135 };
136 let j = serde_json::to_string(&req).expect("serialize");
137 assert!(
138 j.contains("\"methodCalls\""),
139 "must use camelCase methodCalls"
140 );
141 assert!(!j.contains("\"method_calls\""), "must not use snake_case");
142 }
143
144 #[test]
146 fn response_serializes_camelcase() {
147 let resp = JmapResponse {
148 method_responses: vec![],
149 session_state: "s-1".into(),
150 created_ids: None,
151 extra: serde_json::Map::new(),
152 };
153 let j = serde_json::to_string(&resp).expect("serialize");
154 assert!(j.contains("\"methodResponses\""));
155 assert!(j.contains("\"sessionState\""));
156 }
157
158 #[test]
160 fn created_ids_absent_when_none() {
161 let resp = JmapResponse {
162 method_responses: vec![],
163 session_state: "s-1".into(),
164 created_ids: None,
165 extra: serde_json::Map::new(),
166 };
167 let j = serde_json::to_string(&resp).expect("serialize");
168 assert!(
169 !j.contains("createdIds"),
170 "createdIds must be absent when None"
171 );
172 }
173
174 #[test]
176 fn created_ids_present_when_some() {
177 let mut ids = std::collections::HashMap::new();
178 ids.insert(Id::from("c0"), Id::from("server-1"));
179 let resp = JmapResponse {
180 method_responses: vec![],
181 session_state: "s-1".into(),
182 created_ids: Some(ids),
183 extra: serde_json::Map::new(),
184 };
185 let v = serde_json::to_value(&resp).expect("serialize");
186 assert_eq!(v["createdIds"]["c0"], "server-1");
187 }
188
189 #[test]
191 fn invocation_is_three_element_array() {
192 let inv: Invocation = ("m/get".into(), json!({"accountId": "a1"}), "c0".into());
193 let j = serde_json::to_string(&inv).expect("serialize");
194 assert_eq!(j, r#"["m/get",{"accountId":"a1"},"c0"]"#);
195 }
196
197 #[test]
207 fn request_preserves_vendor_extras() {
208 let raw = json!({
209 "using": ["urn:ietf:params:jmap:core"],
210 "methodCalls": [["m1", {}, "c0"]],
211 "acmeCorpClientTraceId": "trace-7"
212 });
213 let req: JmapRequest = serde_json::from_value(raw).unwrap();
214 assert_eq!(
215 req.extra
216 .get("acmeCorpClientTraceId")
217 .and_then(|v| v.as_str()),
218 Some("trace-7"),
219 "vendor field must land in extra: {:?}",
220 req.extra
221 );
222 let back = serde_json::to_value(&req).unwrap();
223 assert_eq!(
224 back["acmeCorpClientTraceId"], "trace-7",
225 "vendor field must survive serialize"
226 );
227 }
228
229 #[test]
232 fn response_preserves_vendor_extras() {
233 let raw = json!({
234 "methodResponses": [],
235 "sessionState": "s-1",
236 "acmeCorpServerHostName": "srv-3"
237 });
238 let resp: JmapResponse = serde_json::from_value(raw).unwrap();
239 assert_eq!(
240 resp.extra
241 .get("acmeCorpServerHostName")
242 .and_then(|v| v.as_str()),
243 Some("srv-3")
244 );
245 let back = serde_json::to_value(&resp).unwrap();
246 assert_eq!(back["acmeCorpServerHostName"], "srv-3");
247 }
248
249 #[test]
252 fn empty_extras_omitted_from_wire() {
253 let req = JmapRequest::new(vec!["urn:ietf:params:jmap:core".into()], vec![], None);
254 let v = serde_json::to_value(&req).expect("serialize");
255 let obj = v.as_object().expect("object");
256 assert_eq!(
259 obj.len(),
260 2,
261 "empty extras + skipped optionals must yield exactly using+methodCalls; got {v}"
262 );
263 assert!(obj.contains_key("using"));
264 assert!(obj.contains_key("methodCalls"));
265 }
266}