Skip to main content

resolve_args

Function resolve_args 

Source
pub fn resolve_args(
    args: &mut Value,
    prior_responses: &[Invocation],
) -> Result<(), JmapError>
Expand description

Resolve all #key ResultReference fields in args against prior_responses.

For every key in args that starts with #:

  1. Parse the value as a ResultReference.
  2. Find the prior response whose call-id matches rr.result_of (index 2 of tuple).
  3. Verify rr.name matches the method name of that response (index 0 of tuple).
  4. Apply rr.path as an RFC 6901 JSON Pointer (with RFC 8620 §3.7 * extension) to the response args (index 1 of tuple).
  5. Collect (plain_key, resolved_value) pairs.

This is two-phase atomic: args is not modified at all unless every #key resolves successfully. If any resolution fails, args is returned unchanged and an error is returned.

prior_responses entries are (method_name, response_args, call_id) — same layout as Invocation.

§Why two-phase? (bd:JMAP-jfia.12 decision record)

A future contributor will reasonably suggest “this is one extra pass over the keys — just resolve-and-mutate inline, that’s simpler”. That suggestion is WRONG. The two-phase structure is load-bearing:

  1. RFC 8620 §3.7 semantics are atomic: if ANY ResultReference fails to resolve, the entire method call gets invalidResultReference. A partial-mutation would leave a malformed args object that handlers could observe.
  2. args is taken by &mut, so it remains observable to the caller after the function returns Err. A test that checks args contents on the error path would see a partial state under inline-mutation.
  3. The test parse::tests::resolve_args_atomic_on_partial_failure directly encodes this contract (“args must be completely unchanged on error”). Without two-phase, that test would fail.

The cost is one Vec<(ref_key, plain_key, resolved_value)> allocation per call; that cost is the price of the atomicity contract.