use crate::{Invocation, JmapError, JmapRequest, ResultReference};
use serde_json::Value;
pub fn parse_request(body: Value, max_calls: usize) -> Result<JmapRequest, JmapError> {
let req: JmapRequest = serde_json::from_value(body).map_err(|_| JmapError::not_request())?;
if req.method_calls.len() > max_calls {
return Err(JmapError::limit("maxCallsInRequest"));
}
Ok(req)
}
pub fn check_known_capabilities<S: AsRef<str>>(
req: &JmapRequest,
known: &[S],
) -> Result<(), JmapError> {
for uri in &req.using {
if !known.iter().any(|k| k.as_ref() == uri.as_str()) {
return Err(JmapError::unknown_capability_with_detail(uri));
}
}
Ok(())
}
pub fn resolve_args(args: &mut Value, prior_responses: &[Invocation]) -> Result<(), JmapError> {
let Some(obj) = args.as_object_mut() else {
return Ok(()); };
let mut ref_pairs: Vec<(String, Value)> = Vec::with_capacity(obj.len());
ref_pairs.extend(
obj.iter()
.filter(|(k, _)| k.starts_with('#'))
.map(|(k, v)| (k.clone(), v.clone())),
);
if ref_pairs.is_empty() {
return Ok(());
}
let mut resolutions: Vec<(String, String, Value)> = Vec::with_capacity(ref_pairs.len());
for (ref_key, ref_value) in ref_pairs {
let plain_key = ref_key[1..].to_owned();
let rr: ResultReference = serde_json::from_value(ref_value).map_err(|e| {
JmapError::invalid_arguments(format!("invalid ResultReference for #{plain_key}: {e}"))
})?;
let (prior_method, prior_value) = prior_responses
.iter()
.find(|(_, _, call_id)| call_id == &rr.result_of)
.map(|(method, value, _)| (method.as_str(), value))
.ok_or_else(JmapError::invalid_result_reference)?;
if rr.name != prior_method {
return Err(JmapError::invalid_result_reference());
}
let resolved = json_pointer_ext(prior_value, &rr.path)
.ok_or_else(JmapError::invalid_result_reference)?;
if obj.contains_key(&plain_key) {
return Err(JmapError::invalid_arguments(format!(
"argument key conflict: '{}' and '#{}' both present",
plain_key, plain_key
)));
}
resolutions.push((ref_key, plain_key, resolved));
}
for (ref_key, plain_key, resolved) in resolutions {
obj.remove(&ref_key);
obj.insert(plain_key, resolved);
}
Ok(())
}
const MAX_JSON_POINTER_DEPTH: usize = 32;
fn json_pointer_ext(value: &Value, path: &str) -> Option<Value> {
json_pointer_ext_inner(value, path, 0)
}
fn json_pointer_ext_inner(value: &Value, path: &str, depth: usize) -> Option<Value> {
if depth > MAX_JSON_POINTER_DEPTH {
return None;
}
if path.is_empty() {
return Some(value.clone());
}
if !path.starts_with('/') {
return None;
}
let after_slash = &path[1..];
let (token, remaining) = match after_slash.find('/') {
Some(pos) => (&after_slash[..pos], &after_slash[pos..]),
None => (after_slash, ""),
};
if token == "*" {
let arr = value.as_array()?;
let mut result: Vec<Value> = Vec::new();
for item in arr {
match json_pointer_ext_inner(item, remaining, depth + 1) {
Some(Value::Array(inner)) => result.extend(inner),
Some(other) => result.push(other),
None => return None, }
}
Some(Value::Array(result))
} else {
let key: std::borrow::Cow<str> = if token.contains('~') {
token.replace("~1", "/").replace("~0", "~").into()
} else {
token.into()
};
let next = match value {
Value::Object(obj) => obj.get(key.as_ref())?,
Value::Array(arr) => {
if key.len() > 1 && key.starts_with('0') {
return None;
}
let idx: usize = key.parse().ok()?;
arr.get(idx)?
}
_ => return None,
};
json_pointer_ext_inner(next, remaining, depth + 1)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn parse_request_valid() {
let body = json!({
"using": ["urn:ietf:params:jmap:core"],
"methodCalls": [
["Foo/get", {"accountId": "a1"}, "0"]
]
});
let req = parse_request(body, 16).expect("valid request must parse");
assert_eq!(req.using, vec!["urn:ietf:params:jmap:core"]);
assert_eq!(req.method_calls.len(), 1);
}
#[test]
fn parse_request_empty_using_is_ok() {
let body = json!({
"using": [],
"methodCalls": []
});
parse_request(body, 16)
.expect("empty using must be accepted — unknownMethod is dispatcher's job");
}
#[test]
fn parse_request_too_many_calls() {
let call = json!(["Foo/get", {}, "0"]);
let calls: Vec<_> = (0..5).map(|_| call.clone()).collect();
let body = json!({
"using": ["urn:ietf:params:jmap:core"],
"methodCalls": calls
});
let err = parse_request(body, 4).unwrap_err();
assert_eq!(
err.error_type, "limit",
"exceeding maxCallsInRequest must return limit per RFC 8620 §3.6.1"
);
}
#[test]
fn parse_request_at_max_calls_is_ok() {
let call = json!(["Foo/get", {}, "0"]);
let calls: Vec<_> = (0..4).map(|_| call.clone()).collect();
let body = json!({
"using": ["urn:ietf:params:jmap:core"],
"methodCalls": calls
});
parse_request(body, 4).expect("exactly max_calls must be accepted");
}
#[test]
fn parse_request_malformed_body() {
let body = json!("not an object");
let err = parse_request(body, 16).unwrap_err();
assert_eq!(
err.error_type, "notRequest",
"malformed body does not match Request type — must be notRequest per RFC 8620 §3.6.1"
);
}
#[test]
fn resolve_args_basic() {
let prior = vec![(
"Foo/get".to_owned(),
json!({"list": [{"id": "x1"}], "state": "s0"}),
"c0".to_owned(),
)];
let mut args = json!({
"#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/list/0/id"}
});
resolve_args(&mut args, &prior).expect("must resolve");
assert_eq!(args, json!({"ids": "x1"}));
}
#[test]
fn resolve_args_unknown_result_of() {
let prior: Vec<Invocation> = vec![];
let mut args = json!({
"#ids": {"resultOf": "missing", "name": "Foo/get", "path": "/ids"}
});
let original = args.clone();
let err = resolve_args(&mut args, &prior).unwrap_err();
assert_eq!(err.error_type, "invalidResultReference");
assert_eq!(args, original);
}
#[test]
fn resolve_args_name_mismatch() {
let prior = vec![("Foo/get".to_owned(), json!({"ids": ["a"]}), "c0".to_owned())];
let mut args = json!({
"#ids": {"resultOf": "c0", "name": "Bar/get", "path": "/ids"}
});
let original = args.clone();
let err = resolve_args(&mut args, &prior).unwrap_err();
assert_eq!(err.error_type, "invalidResultReference");
assert_eq!(args, original);
}
#[test]
fn resolve_args_path_not_found() {
let prior = vec![("Foo/get".to_owned(), json!({"ids": ["a"]}), "c0".to_owned())];
let mut args = json!({
"#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/nonexistent"}
});
let original = args.clone();
let err = resolve_args(&mut args, &prior).unwrap_err();
assert_eq!(err.error_type, "invalidResultReference");
assert_eq!(args, original);
}
#[test]
fn resolve_args_atomic_on_partial_failure() {
let prior = vec![(
"Foo/get".to_owned(),
json!({"ids": ["a", "b"]}),
"c0".to_owned(),
)];
let mut args = json!({
"#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/ids"},
"#properties": {"resultOf": "missing", "name": "Foo/get", "path": "/props"}
});
let original = args.clone();
let err = resolve_args(&mut args, &prior).unwrap_err();
assert_eq!(err.error_type, "invalidResultReference");
assert_eq!(args, original);
}
#[test]
fn resolve_args_non_object_passthrough() {
let prior: Vec<Invocation> = vec![];
let mut args = json!("not-an-object");
resolve_args(&mut args, &prior).expect("non-object must not error");
assert_eq!(args, json!("not-an-object"));
}
#[test]
fn resolve_args_no_ref_keys() {
let prior: Vec<Invocation> = vec![];
let mut args = json!({"ids": ["a", "b"]});
resolve_args(&mut args, &prior).expect("no ref keys must not error");
assert_eq!(args, json!({"ids": ["a", "b"]}));
}
#[test]
fn parse_request_unknown_capability_accepted() {
let body = json!({
"using": ["urn:ietf:params:jmap:core", "urn:example:unknown"],
"methodCalls": [
["Foo/get", {}, "0"]
]
});
let req = parse_request(body, 16).expect("unknown capability must be accepted");
assert_eq!(req.using.len(), 2);
}
#[test]
fn parse_request_core_only_accepted() {
let body = json!({
"using": ["urn:ietf:params:jmap:core"],
"methodCalls": [
["Foo/get", {}, "0"]
]
});
parse_request(body, 16).expect("core-only using must be accepted");
}
#[test]
fn parse_request_zero_max_calls_rejects_any_call() {
let body = json!({
"using": ["urn:ietf:params:jmap:core"],
"methodCalls": [
["Foo/get", {}, "0"]
]
});
let err = parse_request(body, 0).unwrap_err();
assert_eq!(
err.error_type, "limit",
"zero max_calls means any call exceeds limit — must be limit per RFC 8620 §3.6.1"
);
}
#[test]
fn resolve_args_multiple_refs_all_resolve() {
let prior = vec![(
"Foo/get".to_owned(),
json!({"list": [{"id": "x1"}], "state": "s0"}),
"c0".to_owned(),
)];
let mut args = json!({
"#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/list"},
"#state": {"resultOf": "c0", "name": "Foo/get", "path": "/state"}
});
resolve_args(&mut args, &prior).expect("both refs must resolve");
let obj = args.as_object().expect("must still be an object");
assert!(!obj.contains_key("#ids"), "#ids must be removed");
assert!(!obj.contains_key("#state"), "#state must be removed");
assert_eq!(args["ids"], json!([{"id": "x1"}]));
assert_eq!(args["state"], json!("s0"));
}
#[test]
fn resolve_args_key_conflict_is_error() {
let prior = vec![("Foo/get".to_owned(), json!({"ids": ["a"]}), "c0".to_owned())];
let mut args = json!({
"ids": "existing",
"#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/ids"}
});
let original = args.clone();
let err = resolve_args(&mut args, &prior).unwrap_err();
assert_eq!(err.error_type, "invalidArguments");
assert_eq!(args, original);
}
#[test]
fn resolve_args_invalid_ref_value_is_error() {
let prior: Vec<Invocation> = vec![];
let mut args = json!({"#ids": "not-an-object"});
let original = args.clone();
let err = resolve_args(&mut args, &prior).unwrap_err();
assert_eq!(err.error_type, "invalidArguments");
assert_eq!(args, original);
}
#[test]
fn resolve_args_array_path_resolves_to_array() {
let prior = vec![(
"List/query".to_owned(),
json!({"ids": ["a", "b", "c"]}),
"c0".to_owned(),
)];
let mut args = json!({
"#ids": {"resultOf": "c0", "name": "List/query", "path": "/ids"}
});
resolve_args(&mut args, &prior).expect("array path must resolve");
assert_eq!(args, json!({"ids": ["a", "b", "c"]}));
}
#[test]
fn resolve_args_nested_path_resolves() {
let prior = vec![(
"Foo/get".to_owned(),
json!({"list": [{"id": "deep1"}]}),
"c0".to_owned(),
)];
let mut args = json!({
"#id": {"resultOf": "c0", "name": "Foo/get", "path": "/list/0/id"}
});
resolve_args(&mut args, &prior).expect("nested path must resolve");
assert_eq!(args, json!({"id": "deep1"}));
}
#[test]
fn resolve_args_path_array_oob_is_error() {
let prior = vec![("Foo/get".to_owned(), json!({"ids": ["a"]}), "c0".to_owned())];
let mut args = json!({
"#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/ids/5"}
});
let original = args.clone();
let err = resolve_args(&mut args, &prior).unwrap_err();
assert_eq!(err.error_type, "invalidResultReference");
assert_eq!(args, original);
}
#[test]
fn resolve_args_path_leading_zero_index_is_error() {
let prior = vec![(
"Foo/get".to_owned(),
json!({"ids": ["a", "b"]}),
"c0".to_owned(),
)];
let mut args = json!({
"#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/ids/01"}
});
let original = args.clone();
let err = resolve_args(&mut args, &prior).unwrap_err();
assert_eq!(err.error_type, "invalidResultReference");
assert_eq!(args, original, "args must be unchanged on error");
}
#[test]
fn resolve_args_path_tilde_escaping() {
let prior = vec![(
"Foo/get".to_owned(),
json!({"a/b": "slash-value"}),
"c0".to_owned(),
)];
let mut args = json!({
"#val": {"resultOf": "c0", "name": "Foo/get", "path": "/a~1b"}
});
resolve_args(&mut args, &prior).expect("tilde-escaped path must resolve");
assert_eq!(args, json!({"val": "slash-value"}));
}
#[test]
fn resolve_args_path_tilde0_escaping() {
let prior = vec![(
"Foo/get".to_owned(),
json!({"a~b": "tilde-value"}),
"c0".to_owned(),
)];
let mut args = json!({
"#val": {"resultOf": "c0", "name": "Foo/get", "path": "/a~0b"}
});
resolve_args(&mut args, &prior).expect("~0-escaped path must resolve");
assert_eq!(args, json!({"val": "tilde-value"}));
}
#[test]
fn resolve_args_path_tilde01_decodes_to_tilde1() {
let prior = vec![(
"Foo/get".to_owned(),
json!({"~1": "tilde-one-value"}),
"c0".to_owned(),
)];
let mut args = json!({
"#val": {"resultOf": "c0", "name": "Foo/get", "path": "/~01"}
});
resolve_args(&mut args, &prior).expect("~01 must decode to literal key ~1");
assert_eq!(args, json!({"val": "tilde-one-value"}));
}
#[test]
fn resolve_args_wildcard_maps_over_array() {
let prior = vec![(
"Thread/get".to_owned(),
json!({
"list": [{"threadId": "t1"}, {"threadId": "t2"}]
}),
"c0".to_owned(),
)];
let mut args =
json!({"#ids": {"resultOf": "c0", "name": "Thread/get", "path": "/list/*/threadId"}});
resolve_args(&mut args, &prior).expect("wildcard must resolve");
assert_eq!(args, json!({"ids": ["t1", "t2"]}));
}
#[test]
fn resolve_args_wildcard_over_flat_string_array() {
let prior = vec![(
"Email/query".to_owned(),
json!({ "ids": ["a", "b", "c"] }),
"c0".to_owned(),
)];
let mut args = json!({"#ids": {"resultOf": "c0", "name": "Email/query", "path": "/ids/*"}});
resolve_args(&mut args, &prior).expect("flat-array wildcard must resolve");
assert_eq!(args, json!({"ids": ["a", "b", "c"]}));
}
#[test]
fn resolve_args_wildcard_flattens_array_results() {
let prior = vec![(
"Email/get".to_owned(),
json!({
"list": [{"emailIds": ["e1", "e2"]}, {"emailIds": ["e3"]}]
}),
"c0".to_owned(),
)];
let mut args =
json!({"#ids": {"resultOf": "c0", "name": "Email/get", "path": "/list/*/emailIds"}});
resolve_args(&mut args, &prior).expect("wildcard flatten must resolve");
assert_eq!(args, json!({"ids": ["e1", "e2", "e3"]}));
}
#[test]
fn json_pointer_ext_plain_path() {
let v = json!({"a": {"b": 42}});
assert_eq!(json_pointer_ext(&v, "/a/b"), Some(json!(42)));
}
#[test]
fn json_pointer_ext_empty_path_returns_root() {
let v = json!({"x": 1});
assert_eq!(json_pointer_ext(&v, ""), Some(v.clone()));
}
#[test]
fn json_pointer_ext_rejects_deep_path() {
const DEPTH: usize = 1000;
let mut value = json!(42);
for _ in 0..DEPTH {
value = json!({ "a": value });
}
let path: String = "/a".repeat(DEPTH);
assert_eq!(
json_pointer_ext(&value, &path),
None,
"pointer with {DEPTH} tokens must be rejected by the depth cap"
);
}
#[test]
fn json_pointer_ext_accepts_path_within_depth_cap() {
const LEN: usize = MAX_JSON_POINTER_DEPTH - 1;
let mut value = json!("leaf");
for _ in 0..LEN {
value = json!({ "a": value });
}
let path: String = "/a".repeat(LEN);
assert_eq!(
json_pointer_ext(&value, &path),
Some(json!("leaf")),
"pointer with {LEN} tokens must still resolve under the depth cap"
);
}
#[test]
fn check_known_capabilities_unknown_uri_is_error() {
let req = JmapRequest::new(
vec![
"urn:ietf:params:jmap:core".into(),
"urn:example:unknown".into(),
],
vec![],
None,
);
let known = &["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"];
let err = check_known_capabilities(&req, known).unwrap_err();
assert_eq!(
err.error_type, "unknownCapability",
"unrecognised URI must produce unknownCapability per RFC 8620 §3.3"
);
assert_eq!(
err.description.as_deref(),
Some("urn:example:unknown"),
"unknownCapability error must name the unrecognised URI in description"
);
}
#[test]
fn check_known_capabilities_all_known_is_ok() {
let req = JmapRequest::new(
vec![
"urn:ietf:params:jmap:core".into(),
"urn:ietf:params:jmap:mail".into(),
],
vec![],
None,
);
let known = &["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"];
check_known_capabilities(&req, known).expect("all URIs are in known — must return Ok");
}
#[test]
fn check_known_capabilities_empty_using_is_ok() {
let req = JmapRequest::new(vec![], vec![], None);
let known = &["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"];
check_known_capabilities(&req, known)
.expect("empty using[] must return Ok even when known is non-empty");
}
}