use serde::Deserialize;
use serde_json::{Map, Value};
#[derive(Debug, Deserialize, PartialEq, Eq)]
pub struct ResultReference {
#[serde(rename = "resultOf")]
pub result_of: String,
pub name: String,
pub path: String,
}
#[derive(thiserror::Error, Debug)]
pub enum BackRefError {
#[error("result not found: no completed call with id={0:?} and method={1:?}")]
ResultNotFound(String, String),
#[error("referenced call id={0:?} returned an error response")]
ResultWasError(String),
#[error("path {0:?} resolved to nothing in the response for call id={1:?}")]
PathNotFound(String, String),
}
pub fn resolve_back_references(
args: &mut Map<String, Value>,
completed: &[(String, String, Value)],
) -> Result<(), BackRefError> {
let ref_keys: Vec<String> = args
.keys()
.filter(|k| k.starts_with('#'))
.cloned()
.collect();
for hash_key in ref_keys {
let raw = match args.get(&hash_key) {
Some(v) => v.clone(),
None => continue,
};
let reference: ResultReference = match serde_json::from_value(raw) {
Ok(r) => r,
Err(_) => continue,
};
let (_id, _name, response_body) = match completed
.iter()
.find(|(id, name, _)| id == &reference.result_of && name == &reference.name)
{
Some(entry) => entry,
None => {
return Err(BackRefError::ResultNotFound(
reference.result_of,
reference.name,
));
}
};
if is_error_response(response_body) {
return Err(BackRefError::ResultWasError(reference.result_of.clone()));
}
let extracted = match response_body.pointer(&reference.path) {
Some(v) => v.clone(),
None => {
return Err(BackRefError::PathNotFound(
reference.path.clone(),
reference.result_of.clone(),
));
}
};
let plain_key = hash_key[1..].to_string();
args.remove(&hash_key);
args.insert(plain_key, extracted);
}
Ok(())
}
fn is_error_response(body: &Value) -> bool {
body.get("type")
.and_then(Value::as_str)
.map(|t| t.starts_with("urn:ietf:params:jmap:error:"))
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn completed_entry(id: &str, name: &str, body: Value) -> (String, String, Value) {
(id.to_string(), name.to_string(), body)
}
#[test]
fn test_back_ref_resolves_correctly() {
let query_response = json!({
"accountId": "acc1",
"queryState": "s1",
"canCalculateChanges": false,
"position": 0,
"ids": ["email-1", "email-2", "email-3"]
});
let completed = vec![completed_entry("c0", "Email/query", query_response)];
let mut args = serde_json::Map::new();
args.insert("accountId".to_string(), json!("acc1"));
args.insert(
"#ids".to_string(),
json!({
"resultOf": "c0",
"name": "Email/query",
"path": "/ids"
}),
);
resolve_back_references(&mut args, &completed).expect("should resolve");
assert!(!args.contains_key("#ids"), "#ids should have been removed");
assert_eq!(
args["ids"],
json!(["email-1", "email-2", "email-3"]),
"ids should equal the extracted array"
);
assert_eq!(args["accountId"], json!("acc1"));
}
#[test]
fn test_back_ref_resolves_nested_path() {
let email_get_response = json!({
"accountId": "acc1",
"state": "s2",
"list": [
{ "id": "email-1", "threadId": "T1", "subject": "Hello" }
],
"notFound": []
});
let completed = vec![completed_entry("c0", "Email/get", email_get_response)];
let mut args = serde_json::Map::new();
args.insert(
"#threadId".to_string(),
json!({
"resultOf": "c0",
"name": "Email/get",
"path": "/list/0/threadId"
}),
);
resolve_back_references(&mut args, &completed).expect("should resolve nested");
assert!(!args.contains_key("#threadId"));
assert_eq!(args["threadId"], json!("T1"));
}
#[test]
fn test_back_ref_result_not_found() {
let completed: Vec<(String, String, Value)> = vec![];
let mut args = serde_json::Map::new();
args.insert(
"#ids".to_string(),
json!({
"resultOf": "ghost-call",
"name": "Email/query",
"path": "/ids"
}),
);
let err =
resolve_back_references(&mut args, &completed).expect_err("should return an error");
assert!(
matches!(err, BackRefError::ResultNotFound(ref id, _) if id == "ghost-call"),
"unexpected error variant: {err}"
);
}
#[test]
fn test_back_ref_result_not_found_wrong_name() {
let completed = vec![completed_entry(
"c0",
"Email/get",
json!({ "list": [], "notFound": [] }),
)];
let mut args = serde_json::Map::new();
args.insert(
"#ids".to_string(),
json!({
"resultOf": "c0",
"name": "Email/query", "path": "/ids"
}),
);
let err = resolve_back_references(&mut args, &completed)
.expect_err("should return an error for method-name mismatch");
assert!(matches!(err, BackRefError::ResultNotFound(..)));
}
#[test]
fn test_back_ref_path_not_found() {
let completed = vec![completed_entry(
"c0",
"Email/query",
json!({
"accountId": "acc1",
"ids": ["e1"]
}),
)];
let mut args = serde_json::Map::new();
args.insert(
"#missingKey".to_string(),
json!({
"resultOf": "c0",
"name": "Email/query",
"path": "/doesNotExist/deeply/nested"
}),
);
let err =
resolve_back_references(&mut args, &completed).expect_err("should return PathNotFound");
assert!(
matches!(err, BackRefError::PathNotFound(ref p, _) if p == "/doesNotExist/deeply/nested"),
"unexpected error: {err}"
);
}
#[test]
fn test_back_ref_result_was_error() {
let error_body = json!({
"type": "urn:ietf:params:jmap:error:serverFail",
"detail": "something went wrong"
});
let completed = vec![completed_entry("c0", "Email/query", error_body)];
let mut args = serde_json::Map::new();
args.insert(
"#ids".to_string(),
json!({
"resultOf": "c0",
"name": "Email/query",
"path": "/ids"
}),
);
let err = resolve_back_references(&mut args, &completed)
.expect_err("should return ResultWasError");
assert!(
matches!(err, BackRefError::ResultWasError(ref id) if id == "c0"),
"unexpected error: {err}"
);
}
#[test]
fn test_back_ref_non_ref_shape_left_alone() {
let completed: Vec<(String, String, Value)> = vec![];
let mut args = serde_json::Map::new();
args.insert("#notARef".to_string(), json!("plain string value"));
args.insert("#alsoNotARef".to_string(), json!({ "someOtherField": 42 }));
resolve_back_references(&mut args, &completed).expect("should succeed");
assert_eq!(args["#notARef"], json!("plain string value"));
assert_eq!(args["#alsoNotARef"], json!({ "someOtherField": 42 }));
}
}