1use crate::{Invocation, JmapError, JmapRequest, ResultReference};
4use serde_json::Value;
5
6pub fn parse_request(body: Value, max_calls: usize) -> Result<JmapRequest, JmapError> {
32 let req: JmapRequest = serde_json::from_value(body).map_err(|_| JmapError::not_request())?;
33
34 if req.using.is_empty() {
35 return Err(JmapError::not_request());
36 }
37
38 if req.method_calls.len() > max_calls {
39 return Err(JmapError::limit("maxCallsInRequest"));
40 }
41
42 Ok(req)
43}
44
45pub fn resolve_args(args: &mut Value, prior_responses: &[Invocation]) -> Result<(), JmapError> {
62 let Some(obj) = args.as_object_mut() else {
63 return Ok(()); };
65
66 let mut ref_pairs: Vec<(String, Value)> = Vec::with_capacity(obj.len());
69 ref_pairs.extend(
70 obj.iter()
71 .filter(|(k, _)| k.starts_with('#'))
72 .map(|(k, v)| (k.clone(), v.clone())),
73 );
74
75 if ref_pairs.is_empty() {
76 return Ok(());
77 }
78
79 let mut resolutions: Vec<(String, String, Value)> = Vec::with_capacity(ref_pairs.len());
82
83 for (ref_key, ref_value) in ref_pairs {
84 let plain_key = ref_key[1..].to_owned();
85
86 let rr: ResultReference = serde_json::from_value(ref_value).map_err(|e| {
88 JmapError::invalid_arguments(format!("invalid ResultReference for #{plain_key}: {e}"))
89 })?;
90
91 let (prior_method, prior_value) = prior_responses
93 .iter()
94 .find(|(_, _, call_id)| call_id == &rr.result_of)
95 .map(|(method, value, _)| (method.as_str(), value))
96 .ok_or_else(JmapError::invalid_result_reference)?;
97
98 if rr.name != prior_method {
100 return Err(JmapError::invalid_result_reference());
101 }
102
103 let resolved = json_pointer_ext(prior_value, &rr.path)
105 .ok_or_else(JmapError::invalid_result_reference)?;
106
107 if obj.contains_key(&plain_key) {
109 return Err(JmapError::invalid_arguments(format!(
110 "argument key conflict: '{}' and '#{}' both present",
111 plain_key, plain_key
112 )));
113 }
114
115 resolutions.push((ref_key, plain_key, resolved));
116 }
117
118 for (ref_key, plain_key, resolved) in resolutions {
120 obj.remove(&ref_key);
121 obj.insert(plain_key, resolved);
122 }
123
124 Ok(())
125}
126
127fn json_pointer_ext(value: &Value, path: &str) -> Option<Value> {
133 if path.is_empty() {
134 return Some(value.clone());
135 }
136 if !path.starts_with('/') {
137 return None;
138 }
139
140 let after_slash = &path[1..];
142 let (token, remaining) = match after_slash.find('/') {
143 Some(pos) => (&after_slash[..pos], &after_slash[pos..]),
144 None => (after_slash, ""),
145 };
146
147 if token == "*" {
148 let arr = value.as_array()?;
150 let mut result: Vec<Value> = Vec::new();
151 for item in arr {
152 match json_pointer_ext(item, remaining) {
153 Some(Value::Array(inner)) => result.extend(inner),
154 Some(other) => result.push(other),
155 None => return None, }
157 }
158 Some(Value::Array(result))
159 } else {
160 let key: std::borrow::Cow<str> = if token.contains('~') {
163 token.replace("~1", "/").replace("~0", "~").into()
164 } else {
165 token.into()
166 };
167 let next = match value {
168 Value::Object(obj) => obj.get(key.as_ref())?,
169 Value::Array(arr) => {
170 if key.len() > 1 && key.starts_with('0') {
172 return None;
173 }
174 let idx: usize = key.parse().ok()?;
175 arr.get(idx)?
176 }
177 _ => return None,
178 };
179 json_pointer_ext(next, remaining)
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use serde_json::json;
187
188 #[test]
191 fn parse_request_valid() {
192 let body = json!({
193 "using": ["urn:ietf:params:jmap:core"],
194 "methodCalls": [
195 ["Foo/get", {"accountId": "a1"}, "0"]
196 ]
197 });
198 let req = parse_request(body, 16).expect("valid request must parse");
199 assert_eq!(req.using, vec!["urn:ietf:params:jmap:core"]);
200 assert_eq!(req.method_calls.len(), 1);
201 }
202
203 #[test]
204 fn parse_request_empty_using_is_error() {
205 let body = json!({
206 "using": [],
207 "methodCalls": []
208 });
209 let err = parse_request(body, 16).unwrap_err();
210 assert_eq!(
211 err.error_type, "notRequest",
212 "empty using violates request structure — must be notRequest per RFC 8620 §3.6.1"
213 );
214 }
215
216 #[test]
217 fn parse_request_too_many_calls() {
218 let call = json!(["Foo/get", {}, "0"]);
219 let calls: Vec<_> = (0..5).map(|_| call.clone()).collect();
220 let body = json!({
221 "using": ["urn:ietf:params:jmap:core"],
222 "methodCalls": calls
223 });
224 let err = parse_request(body, 4).unwrap_err();
225 assert_eq!(
226 err.error_type, "limit",
227 "exceeding maxCallsInRequest must return limit per RFC 8620 §3.6.1"
228 );
229 }
230
231 #[test]
232 fn parse_request_at_max_calls_is_ok() {
233 let call = json!(["Foo/get", {}, "0"]);
234 let calls: Vec<_> = (0..4).map(|_| call.clone()).collect();
235 let body = json!({
236 "using": ["urn:ietf:params:jmap:core"],
237 "methodCalls": calls
238 });
239 parse_request(body, 4).expect("exactly max_calls must be accepted");
240 }
241
242 #[test]
243 fn parse_request_malformed_body() {
244 let body = json!("not an object");
245 let err = parse_request(body, 16).unwrap_err();
246 assert_eq!(
247 err.error_type, "notRequest",
248 "malformed body does not match Request type — must be notRequest per RFC 8620 §3.6.1"
249 );
250 }
251
252 #[test]
254 fn resolve_args_basic() {
255 let prior = vec![(
256 "Foo/get".to_owned(),
257 json!({"list": [{"id": "x1"}], "state": "s0"}),
258 "c0".to_owned(),
259 )];
260 let mut args = json!({
261 "#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/list/0/id"}
262 });
263 resolve_args(&mut args, &prior).expect("must resolve");
264 assert_eq!(args, json!({"ids": "x1"}));
265 }
266
267 #[test]
269 fn resolve_args_unknown_result_of() {
270 let prior: Vec<Invocation> = vec![];
271 let mut args = json!({
272 "#ids": {"resultOf": "missing", "name": "Foo/get", "path": "/ids"}
273 });
274 let original = args.clone();
275 let err = resolve_args(&mut args, &prior).unwrap_err();
276 assert_eq!(err.error_type, "invalidResultReference");
277 assert_eq!(args, original);
279 }
280
281 #[test]
283 fn resolve_args_name_mismatch() {
284 let prior = vec![("Foo/get".to_owned(), json!({"ids": ["a"]}), "c0".to_owned())];
285 let mut args = json!({
286 "#ids": {"resultOf": "c0", "name": "Bar/get", "path": "/ids"}
287 });
288 let original = args.clone();
289 let err = resolve_args(&mut args, &prior).unwrap_err();
290 assert_eq!(err.error_type, "invalidResultReference");
291 assert_eq!(args, original);
292 }
293
294 #[test]
296 fn resolve_args_path_not_found() {
297 let prior = vec![("Foo/get".to_owned(), json!({"ids": ["a"]}), "c0".to_owned())];
298 let mut args = json!({
299 "#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/nonexistent"}
300 });
301 let original = args.clone();
302 let err = resolve_args(&mut args, &prior).unwrap_err();
303 assert_eq!(err.error_type, "invalidResultReference");
304 assert_eq!(args, original);
305 }
306
307 #[test]
309 fn resolve_args_atomic_on_partial_failure() {
310 let prior = vec![(
311 "Foo/get".to_owned(),
312 json!({"ids": ["a", "b"]}),
313 "c0".to_owned(),
314 )];
315 let mut args = json!({
317 "#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/ids"},
318 "#properties": {"resultOf": "missing", "name": "Foo/get", "path": "/props"}
319 });
320 let original = args.clone();
321 let err = resolve_args(&mut args, &prior).unwrap_err();
322 assert_eq!(err.error_type, "invalidResultReference");
323 assert_eq!(args, original);
324 }
325
326 #[test]
328 fn resolve_args_non_object_passthrough() {
329 let prior: Vec<Invocation> = vec![];
330 let mut args = json!("not-an-object");
331 resolve_args(&mut args, &prior).expect("non-object must not error");
332 assert_eq!(args, json!("not-an-object"));
333 }
334
335 #[test]
337 fn resolve_args_no_ref_keys() {
338 let prior: Vec<Invocation> = vec![];
339 let mut args = json!({"ids": ["a", "b"]});
340 resolve_args(&mut args, &prior).expect("no ref keys must not error");
341 assert_eq!(args, json!({"ids": ["a", "b"]}));
342 }
343
344 #[test]
347 fn parse_request_unknown_capability_accepted() {
348 let body = json!({
349 "using": ["urn:ietf:params:jmap:core", "urn:example:unknown"],
350 "methodCalls": [
351 ["Foo/get", {}, "0"]
352 ]
353 });
354 let req = parse_request(body, 16).expect("unknown capability must be accepted");
355 assert_eq!(req.using.len(), 2);
356 }
357
358 #[test]
360 fn parse_request_core_only_accepted() {
361 let body = json!({
362 "using": ["urn:ietf:params:jmap:core"],
363 "methodCalls": [
364 ["Foo/get", {}, "0"]
365 ]
366 });
367 parse_request(body, 16).expect("core-only using must be accepted");
368 }
369
370 #[test]
372 fn parse_request_zero_max_calls_rejects_any_call() {
373 let body = json!({
374 "using": ["urn:ietf:params:jmap:core"],
375 "methodCalls": [
376 ["Foo/get", {}, "0"]
377 ]
378 });
379 let err = parse_request(body, 0).unwrap_err();
380 assert_eq!(
381 err.error_type, "limit",
382 "zero max_calls means any call exceeds limit — must be limit per RFC 8620 §3.6.1"
383 );
384 }
385
386 #[test]
389 fn resolve_args_multiple_refs_all_resolve() {
390 let prior = vec![(
391 "Foo/get".to_owned(),
392 json!({"list": [{"id": "x1"}], "state": "s0"}),
393 "c0".to_owned(),
394 )];
395 let mut args = json!({
396 "#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/list"},
397 "#state": {"resultOf": "c0", "name": "Foo/get", "path": "/state"}
398 });
399 resolve_args(&mut args, &prior).expect("both refs must resolve");
400 let obj = args.as_object().expect("must still be an object");
402 assert!(!obj.contains_key("#ids"), "#ids must be removed");
403 assert!(!obj.contains_key("#state"), "#state must be removed");
404 assert_eq!(args["ids"], json!([{"id": "x1"}]));
405 assert_eq!(args["state"], json!("s0"));
406 }
407
408 #[test]
411 fn resolve_args_key_conflict_is_error() {
412 let prior = vec![("Foo/get".to_owned(), json!({"ids": ["a"]}), "c0".to_owned())];
413 let mut args = json!({
414 "ids": "existing",
415 "#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/ids"}
416 });
417 let original = args.clone();
418 let err = resolve_args(&mut args, &prior).unwrap_err();
419 assert_eq!(err.error_type, "invalidArguments");
420 assert_eq!(args, original);
422 }
423
424 #[test]
427 fn resolve_args_invalid_ref_value_is_error() {
428 let prior: Vec<Invocation> = vec![];
429 let mut args = json!({"#ids": "not-an-object"});
430 let original = args.clone();
431 let err = resolve_args(&mut args, &prior).unwrap_err();
432 assert_eq!(err.error_type, "invalidArguments");
433 assert_eq!(args, original);
434 }
435
436 #[test]
439 fn resolve_args_array_path_resolves_to_array() {
440 let prior = vec![(
441 "List/query".to_owned(),
442 json!({"ids": ["a", "b", "c"]}),
443 "c0".to_owned(),
444 )];
445 let mut args = json!({
446 "#ids": {"resultOf": "c0", "name": "List/query", "path": "/ids"}
447 });
448 resolve_args(&mut args, &prior).expect("array path must resolve");
449 assert_eq!(args, json!({"ids": ["a", "b", "c"]}));
450 }
451
452 #[test]
455 fn resolve_args_nested_path_resolves() {
456 let prior = vec![(
457 "Foo/get".to_owned(),
458 json!({"list": [{"id": "deep1"}]}),
459 "c0".to_owned(),
460 )];
461 let mut args = json!({
462 "#id": {"resultOf": "c0", "name": "Foo/get", "path": "/list/0/id"}
463 });
464 resolve_args(&mut args, &prior).expect("nested path must resolve");
465 assert_eq!(args, json!({"id": "deep1"}));
466 }
467
468 #[test]
471 fn resolve_args_path_array_oob_is_error() {
472 let prior = vec![("Foo/get".to_owned(), json!({"ids": ["a"]}), "c0".to_owned())];
473 let mut args = json!({
474 "#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/ids/5"}
475 });
476 let original = args.clone();
477 let err = resolve_args(&mut args, &prior).unwrap_err();
478 assert_eq!(err.error_type, "invalidResultReference");
479 assert_eq!(args, original);
480 }
481
482 #[test]
485 fn resolve_args_path_leading_zero_index_is_error() {
486 let prior = vec![(
487 "Foo/get".to_owned(),
488 json!({"ids": ["a", "b"]}),
489 "c0".to_owned(),
490 )];
491 let mut args = json!({
492 "#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/ids/01"}
493 });
494 let original = args.clone();
495 let err = resolve_args(&mut args, &prior).unwrap_err();
496 assert_eq!(err.error_type, "invalidResultReference");
497 assert_eq!(args, original, "args must be unchanged on error");
498 }
499
500 #[test]
503 fn resolve_args_path_tilde_escaping() {
504 let prior = vec![(
505 "Foo/get".to_owned(),
506 json!({"a/b": "slash-value"}),
507 "c0".to_owned(),
508 )];
509 let mut args = json!({
510 "#val": {"resultOf": "c0", "name": "Foo/get", "path": "/a~1b"}
511 });
512 resolve_args(&mut args, &prior).expect("tilde-escaped path must resolve");
513 assert_eq!(args, json!({"val": "slash-value"}));
514 }
515
516 #[test]
520 fn resolve_args_path_tilde0_escaping() {
521 let prior = vec![(
522 "Foo/get".to_owned(),
523 json!({"a~b": "tilde-value"}),
524 "c0".to_owned(),
525 )];
526 let mut args = json!({
527 "#val": {"resultOf": "c0", "name": "Foo/get", "path": "/a~0b"}
528 });
529 resolve_args(&mut args, &prior).expect("~0-escaped path must resolve");
530 assert_eq!(args, json!({"val": "tilde-value"}));
531 }
532
533 #[test]
541 fn resolve_args_path_tilde01_decodes_to_tilde1() {
542 let prior = vec![(
543 "Foo/get".to_owned(),
544 json!({"~1": "tilde-one-value"}),
545 "c0".to_owned(),
546 )];
547 let mut args = json!({
548 "#val": {"resultOf": "c0", "name": "Foo/get", "path": "/~01"}
549 });
550 resolve_args(&mut args, &prior).expect("~01 must decode to literal key ~1");
551 assert_eq!(args, json!({"val": "tilde-one-value"}));
552 }
553
554 #[test]
556 fn resolve_args_wildcard_maps_over_array() {
557 let prior = vec![(
558 "Thread/get".to_owned(),
559 json!({
560 "list": [{"threadId": "t1"}, {"threadId": "t2"}]
561 }),
562 "c0".to_owned(),
563 )];
564 let mut args =
565 json!({"#ids": {"resultOf": "c0", "name": "Thread/get", "path": "/list/*/threadId"}});
566 resolve_args(&mut args, &prior).expect("wildcard must resolve");
567 assert_eq!(args, json!({"ids": ["t1", "t2"]}));
568 }
569
570 #[test]
572 fn resolve_args_wildcard_flattens_array_results() {
573 let prior = vec![(
574 "Email/get".to_owned(),
575 json!({
576 "list": [{"emailIds": ["e1", "e2"]}, {"emailIds": ["e3"]}]
577 }),
578 "c0".to_owned(),
579 )];
580 let mut args =
581 json!({"#ids": {"resultOf": "c0", "name": "Email/get", "path": "/list/*/emailIds"}});
582 resolve_args(&mut args, &prior).expect("wildcard flatten must resolve");
583 assert_eq!(args, json!({"ids": ["e1", "e2", "e3"]}));
584 }
585
586 #[test]
588 fn json_pointer_ext_plain_path() {
589 let v = json!({"a": {"b": 42}});
590 assert_eq!(json_pointer_ext(&v, "/a/b"), Some(json!(42)));
591 }
592
593 #[test]
595 fn json_pointer_ext_empty_path_returns_root() {
596 let v = json!({"x": 1});
597 assert_eq!(json_pointer_ext(&v, ""), Some(v.clone()));
598 }
599}