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    Value(T),
106    Ref(ResultReference),
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn result_reference_round_trip() {
115        // Independent oracle: RFC 8620 §9 example
116        let rr = ResultReference {
117            result_of: "0".into(),
118            name: "ChatContact/get".into(),
119            path: "/list/0/id".into(),
120        };
121        let json_str = serde_json::to_string(&rr).unwrap();
122        let rr2: ResultReference = serde_json::from_str(&json_str).unwrap();
123        assert_eq!(rr, rr2);
124    }
125
126    #[test]
127    fn result_reference_field_names() {
128        let rr = ResultReference {
129            result_of: "req-1".into(),
130            name: "Chat/get".into(),
131            path: "/id".into(),
132        };
133        let json_str = serde_json::to_string(&rr).unwrap();
134        assert!(
135            json_str.contains("\"resultOf\""),
136            "must use camelCase resultOf"
137        );
138        assert!(json_str.contains("\"name\""));
139        assert!(json_str.contains("\"path\""));
140        assert!(
141            !json_str.contains("\"result_of\""),
142            "must not use snake_case"
143        );
144    }
145
146    #[test]
147    fn argument_value_serializes_as_inner_type() {
148        let arg: Argument<u32> = Argument::Value(42);
149        let json_str = serde_json::to_string(&arg).unwrap();
150        assert_eq!(json_str, "42");
151    }
152
153    #[test]
154    fn argument_ref_serializes_as_result_reference() {
155        let rr = ResultReference {
156            result_of: "0".into(),
157            name: "ChatContact/get".into(),
158            path: "/list/0/id".into(),
159        };
160        let arg: Argument<Vec<String>> = Argument::Ref(rr);
161        let json_str = serde_json::to_string(&arg).unwrap();
162        assert!(json_str.contains("\"resultOf\""));
163        assert!(json_str.contains("\"ChatContact/get\""));
164    }
165
166    #[test]
167    fn argument_value_vec_string_deserializes() {
168        let json_str = r#"["alice","bob"]"#;
169        let arg: Argument<Vec<String>> = serde_json::from_str(json_str).unwrap();
170        match arg {
171            Argument::Value(v) => assert_eq!(v, vec!["alice", "bob"]),
172            Argument::Ref(_) => panic!("expected Value variant"),
173        }
174    }
175
176    // Oracle: tests/fixtures/rfc8620-result-reference.json (RFC 8620 §9)
177    #[test]
178    fn result_reference_deserializes_from_fixture() {
179        let raw = include_str!("../tests/fixtures/rfc8620-result-reference.json");
180        let rr: ResultReference = serde_json::from_str(raw).expect("deserialize ResultReference");
181        assert_eq!(rr.result_of, "t0");
182        assert_eq!(rr.name, "Foo/changes");
183        assert_eq!(rr.path, "/created");
184    }
185
186    // Oracle: #[serde(untagged)] — ResultReference JSON object deserializes as Argument::Ref.
187    #[test]
188    fn argument_ref_deserializes() {
189        let j = r#"{"resultOf":"0","name":"X/get","path":"/ids"}"#;
190        let arg: Argument<Vec<String>> = serde_json::from_str(j).expect("deser");
191        match arg {
192            Argument::Ref(rr) => {
193                assert_eq!(rr.result_of, "0");
194                assert_eq!(rr.name, "X/get");
195            }
196            Argument::Value(_) => panic!("expected Ref"),
197        }
198    }
199}