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}