Skip to main content

jmap_types/
resultref.rs

1//! RFC 8620 §7 ResultReference — back-references between method calls in a batch.
2//!
3//! Provides [`ResultReference`] and [`Argument<T>`].
4
5use serde::{Deserialize, Serialize};
6
7/// Sealed marker for types that are safe to use with [`Argument<T>`].
8///
9/// This trait is intentionally sealed: it is defined in a private module and
10/// can only be implemented inside this crate. The current sealed set is:
11/// `String`, `Vec<String>`, `Id`, `Vec<Id>`, `u32`, `u64`, `bool`.
12///
13/// **External contributors**: you cannot implement `Sealed` outside this crate.
14/// To add a new type to the sealed set, open a PR to `crate-jmap-types` and
15/// add the impl here. Before doing so, read the `# Invariant` section on
16/// [`Argument`] — the new type must not deserialize from a JSON object.
17mod sealed {
18    pub trait Sealed {}
19
20    impl Sealed for String {}
21    impl Sealed for Vec<String> {}
22    impl Sealed for crate::Id {}
23    impl Sealed for Vec<crate::Id> {}
24    impl Sealed for u32 {}
25    impl Sealed for u64 {}
26    impl Sealed for bool {}
27}
28
29/// A reference to the result of a previous invocation in the same JMAP request batch.
30/// Used in method arguments with a "#" prefix on the JSON key (RFC 8620 §9).
31///
32/// Example JSON:
33/// ```json
34/// {"resultOf": "0", "name": "ChatContact/get", "path": "/list/0/id"}
35/// ```
36#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
37#[non_exhaustive]
38pub struct ResultReference {
39    /// The call-id of the prior method invocation being referenced.
40    #[serde(rename = "resultOf")]
41    pub result_of: String,
42    /// The method name of that prior invocation (e.g. "ChatContact/get").
43    pub name: String,
44    /// JSON Pointer (RFC 6901) into the result, e.g. "/list/0/id".
45    pub path: String,
46}
47
48impl ResultReference {
49    /// Construct a `ResultReference` from its three required fields.
50    ///
51    /// `result_of`: call-id of the prior method invocation
52    /// `name`: method name of that invocation (e.g. `"Foo/get"`)
53    /// `path`: RFC 6901 JSON Pointer into the result (e.g. `"/list/*/id"`)
54    pub fn new(
55        result_of: impl Into<String>,
56        name: impl Into<String>,
57        path: impl Into<String>,
58    ) -> Self {
59        Self {
60            result_of: result_of.into(),
61            name: name.into(),
62            path: path.into(),
63        }
64    }
65}
66
67/// A JMAP method argument that can be either a direct value or a ResultReference.
68///
69/// In JMAP JSON, a ResultReference is indicated by a "#" prefix on the key:
70///   `"ids": [...]`  →  Argument::Value([...])
71///   `"#ids": {...}` →  Argument::Ref(ResultReference { ... })
72///
73/// The resolver in kith-jmap evaluates Ref variants before method dispatch.
74///
75/// # Deserialization note
76/// Uses `#[serde(untagged)]` which tries to deserialize as T first.
77/// If T and ResultReference share field names, T is preferred.
78/// Callers must handle the `#` key prefix before deserializing.
79///
80/// # Invariant
81///
82/// `T` must not deserialize from a JSON object with keys `resultOf`, `name`,
83/// and `path`. If `T` accepts arbitrary JSON objects, `#[serde(untagged)]`
84/// will match `T` before trying `Ref`, silently swallowing any
85/// `ResultReference` payload. The sealed set is chosen specifically to exclude
86/// struct types for this reason. Never implement `Sealed` for a type that
87/// deserializes from a JSON object.
88///
89/// # Type safety
90/// `T` is constrained to `sealed::Sealed` types. `serde_json::Value` does NOT
91/// implement `Sealed` — `Argument<Value>` therefore fails to compile. This
92/// prevents a silent bug: `Value` accepts any JSON, so with `T = Value` the
93/// `Ref` variant can never be reached via serde; every ResultReference JSON
94/// would deserialize as `Argument::Value(json_object)` and the reference would
95/// be silently ignored.
96///
97/// ```compile_fail
98/// use jmap_types::resultref::Argument;
99/// // serde_json::Value does not implement Sealed — this must not compile.
100/// let _: Argument<serde_json::Value> = Argument::Value(serde_json::Value::Null);
101/// ```
102#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
103#[serde(untagged)]
104pub enum Argument<T: sealed::Sealed> {
105    /// A direct, inlined argument value supplied by the caller.
106    Value(T),
107    /// A result-reference back to a previous method response in the same
108    /// request (RFC 8620 §3.7), resolved by the dispatcher before invoking
109    /// the method handler.
110    Ref(ResultReference),
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn result_reference_round_trip() {
119        // Independent oracle: RFC 8620 §9 example
120        let rr = ResultReference {
121            result_of: "0".into(),
122            name: "ChatContact/get".into(),
123            path: "/list/0/id".into(),
124        };
125        let json_str = serde_json::to_string(&rr).unwrap();
126        let rr2: ResultReference = serde_json::from_str(&json_str).unwrap();
127        assert_eq!(rr, rr2);
128    }
129
130    #[test]
131    fn result_reference_field_names() {
132        let rr = ResultReference {
133            result_of: "req-1".into(),
134            name: "Chat/get".into(),
135            path: "/id".into(),
136        };
137        let json_str = serde_json::to_string(&rr).unwrap();
138        assert!(
139            json_str.contains("\"resultOf\""),
140            "must use camelCase resultOf"
141        );
142        assert!(json_str.contains("\"name\""));
143        assert!(json_str.contains("\"path\""));
144        assert!(
145            !json_str.contains("\"result_of\""),
146            "must not use snake_case"
147        );
148    }
149
150    #[test]
151    fn argument_value_serializes_as_inner_type() {
152        let arg: Argument<u32> = Argument::Value(42);
153        let json_str = serde_json::to_string(&arg).unwrap();
154        assert_eq!(json_str, "42");
155    }
156
157    #[test]
158    fn argument_ref_serializes_as_result_reference() {
159        let rr = ResultReference {
160            result_of: "0".into(),
161            name: "ChatContact/get".into(),
162            path: "/list/0/id".into(),
163        };
164        let arg: Argument<Vec<String>> = Argument::Ref(rr);
165        let json_str = serde_json::to_string(&arg).unwrap();
166        assert!(json_str.contains("\"resultOf\""));
167        assert!(json_str.contains("\"ChatContact/get\""));
168    }
169
170    #[test]
171    fn argument_value_vec_string_deserializes() {
172        let json_str = r#"["alice","bob"]"#;
173        let arg: Argument<Vec<String>> = serde_json::from_str(json_str).unwrap();
174        match arg {
175            Argument::Value(v) => assert_eq!(v, vec!["alice", "bob"]),
176            Argument::Ref(_) => panic!("expected Value variant"),
177        }
178    }
179
180    // Oracle: tests/fixtures/rfc8620-result-reference.json (RFC 8620 §9)
181    #[test]
182    fn result_reference_deserializes_from_fixture() {
183        let raw = include_str!("../tests/fixtures/rfc8620-result-reference.json");
184        let rr: ResultReference = serde_json::from_str(raw).expect("deserialize ResultReference");
185        assert_eq!(rr.result_of, "t0");
186        assert_eq!(rr.name, "Foo/changes");
187        assert_eq!(rr.path, "/created");
188    }
189
190    // Oracle: #[serde(untagged)] — ResultReference JSON object deserializes as Argument::Ref.
191    #[test]
192    fn argument_ref_deserializes() {
193        let j = r#"{"resultOf":"0","name":"X/get","path":"/ids"}"#;
194        let arg: Argument<Vec<String>> = serde_json::from_str(j).expect("deser");
195        match arg {
196            Argument::Ref(rr) => {
197                assert_eq!(rr.result_of, "0");
198                assert_eq!(rr.name, "X/get");
199            }
200            Argument::Value(_) => panic!("expected Ref"),
201        }
202    }
203}