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///
12/// | Type | Rationale |
13/// |---|---|
14/// | `String` | RFC 8620 §3.7 — generic id / token argument |
15/// | `Vec<String>` | id-list argument, e.g. `Email/get { ids }` |
16/// | [`crate::Id`] | typed id argument |
17/// | `Vec<`[`crate::Id`]`>` | typed id-list argument, e.g. `Email/set { destroy }` |
18/// | [`crate::Date`] | calendar-date argument (RFC 8984 / JMAP Calendars) |
19/// | `Vec<`[`crate::Date`]`>` | date-list argument |
20/// | [`crate::UTCDate`] | UTC-timestamp argument, e.g. `Email/query { before, after }` (RFC 8621 §4.4.1) |
21/// | `Vec<`[`crate::UTCDate`]`>` | timestamp-list argument |
22/// | [`crate::State`] | state-token argument, e.g. `Email/changes { sinceState }` (RFC 8620 §5.2) |
23/// | `Vec<`[`crate::State`]`>` | state-list argument |
24/// | `u32`, `u64` | counts, indexes, limits (RFC 8620 §5.5) |
25/// | `bool` | flag argument |
26///
27/// `crate::Date`, `crate::UTCDate`, and `crate::State` are
28/// `#[serde(transparent)]` newtypes over `String`. They serialize and
29/// deserialize as JSON strings (never as objects), so they satisfy the
30/// [`Argument`] `# Invariant` and are safe to seal.
31///
32/// **Types NOT in the sealed set** (and the reason): every JMAP wire
33/// data-object type (`Email`, `Mailbox`, `Calendar`, `ContactCard`,
34/// `Thread`, etc.) and every per-extension argument struct. These types
35/// deserialize from JSON objects; with `#[serde(untagged)]` on
36/// [`Argument`], the `T` variant would match before `Ref` and silently
37/// swallow any `ResultReference` payload. `serde_json::Value` is
38/// excluded for the same reason — see the `# Type safety` section on
39/// [`Argument`].
40///
41/// **External contributors**: you cannot implement `Sealed` outside this crate.
42/// To add a new type to the sealed set, open a PR to `crate-jmap-types` and
43/// add the impl here. Before doing so, read the `# Invariant` section on
44/// [`Argument`] — the new type must not deserialize from a JSON object.
45mod sealed {
46 pub trait Sealed {}
47
48 impl Sealed for String {}
49 impl Sealed for Vec<String> {}
50 impl Sealed for crate::Id {}
51 impl Sealed for Vec<crate::Id> {}
52 impl Sealed for crate::Date {}
53 impl Sealed for Vec<crate::Date> {}
54 impl Sealed for crate::UTCDate {}
55 impl Sealed for Vec<crate::UTCDate> {}
56 impl Sealed for crate::State {}
57 impl Sealed for Vec<crate::State> {}
58 impl Sealed for u32 {}
59 impl Sealed for u64 {}
60 impl Sealed for bool {}
61}
62
63/// A reference to the result of a previous invocation in the same JMAP request batch.
64/// Used in method arguments with a "#" prefix on the JSON key (RFC 8620 §9).
65///
66/// Example JSON:
67/// ```json
68/// {"resultOf": "0", "name": "ChatContact/get", "path": "/list/0/id"}
69/// ```
70#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
71#[non_exhaustive]
72pub struct ResultReference {
73 /// The call-id of the prior method invocation being referenced.
74 #[serde(rename = "resultOf")]
75 pub result_of: String,
76 /// The method name of that prior invocation (e.g. "ChatContact/get").
77 pub name: String,
78 /// JSON Pointer (RFC 6901) into the result, e.g. "/list/0/id".
79 pub path: String,
80}
81
82impl ResultReference {
83 /// Construct a `ResultReference` from its three required fields.
84 ///
85 /// `result_of`: call-id of the prior method invocation
86 /// `name`: method name of that invocation (e.g. `"Foo/get"`)
87 /// `path`: RFC 6901 JSON Pointer into the result (e.g. `"/list/*/id"`)
88 pub fn new(
89 result_of: impl Into<String>,
90 name: impl Into<String>,
91 path: impl Into<String>,
92 ) -> Self {
93 Self {
94 result_of: result_of.into(),
95 name: name.into(),
96 path: path.into(),
97 }
98 }
99}
100
101/// A JMAP method argument that can be either a direct value or a ResultReference.
102///
103/// In JMAP JSON, a ResultReference is indicated by a "#" prefix on the key:
104/// `"ids": [...]` → Argument::Value([...])
105/// `"#ids": {...}` → Argument::Ref(ResultReference { ... })
106///
107/// The dispatcher in `jmap-server` evaluates `Ref` variants before
108/// invoking the method handler.
109///
110/// # Deserialization note
111/// Uses `#[serde(untagged)]` which tries to deserialize as T first.
112/// If T and ResultReference share field names, T is preferred.
113/// Callers must handle the `#` key prefix before deserializing.
114///
115/// # Invariant
116///
117/// `T` must not deserialize from a JSON object with keys `resultOf`, `name`,
118/// and `path`. If `T` accepts arbitrary JSON objects, `#[serde(untagged)]`
119/// will match `T` before trying `Ref`, silently swallowing any
120/// `ResultReference` payload. The sealed set is chosen specifically to exclude
121/// struct types for this reason. Never implement `Sealed` for a type that
122/// deserializes from a JSON object.
123///
124/// # Type safety
125/// `T` is constrained to `sealed::Sealed` types. `serde_json::Value` does NOT
126/// implement `Sealed` — `Argument<Value>` therefore fails to compile. This
127/// prevents a silent bug: `Value` accepts any JSON, so with `T = Value` the
128/// `Ref` variant can never be reached via serde; every ResultReference JSON
129/// would deserialize as `Argument::Value(json_object)` and the reference would
130/// be silently ignored.
131///
132/// ```compile_fail
133/// use jmap_types::resultref::Argument;
134/// // serde_json::Value does not implement Sealed — this must not compile.
135/// let _: Argument<serde_json::Value> = Argument::Value(serde_json::Value::Null);
136/// ```
137#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
138#[serde(untagged)]
139pub enum Argument<T: sealed::Sealed> {
140 /// A direct, inlined argument value supplied by the caller.
141 Value(T),
142 /// A result-reference back to a previous method response in the same
143 /// request (RFC 8620 §3.7), resolved by the dispatcher before invoking
144 /// the method handler.
145 Ref(ResultReference),
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn result_reference_round_trip() {
154 // Independent oracle: RFC 8620 §9 example
155 let rr = ResultReference {
156 result_of: "0".into(),
157 name: "ChatContact/get".into(),
158 path: "/list/0/id".into(),
159 };
160 let json_str = serde_json::to_string(&rr).unwrap();
161 let rr2: ResultReference = serde_json::from_str(&json_str).unwrap();
162 assert_eq!(rr, rr2);
163 }
164
165 #[test]
166 fn result_reference_field_names() {
167 let rr = ResultReference {
168 result_of: "req-1".into(),
169 name: "Chat/get".into(),
170 path: "/id".into(),
171 };
172 let json_str = serde_json::to_string(&rr).unwrap();
173 assert!(
174 json_str.contains("\"resultOf\""),
175 "must use camelCase resultOf"
176 );
177 assert!(json_str.contains("\"name\""));
178 assert!(json_str.contains("\"path\""));
179 assert!(
180 !json_str.contains("\"result_of\""),
181 "must not use snake_case"
182 );
183 }
184
185 #[test]
186 fn argument_value_serializes_as_inner_type() {
187 let arg: Argument<u32> = Argument::Value(42);
188 let json_str = serde_json::to_string(&arg).unwrap();
189 assert_eq!(json_str, "42");
190 }
191
192 #[test]
193 fn argument_ref_serializes_as_result_reference() {
194 let rr = ResultReference {
195 result_of: "0".into(),
196 name: "ChatContact/get".into(),
197 path: "/list/0/id".into(),
198 };
199 let arg: Argument<Vec<String>> = Argument::Ref(rr);
200 let json_str = serde_json::to_string(&arg).unwrap();
201 assert!(json_str.contains("\"resultOf\""));
202 assert!(json_str.contains("\"ChatContact/get\""));
203 }
204
205 #[test]
206 fn argument_value_vec_string_deserializes() {
207 let json_str = r#"["alice","bob"]"#;
208 let arg: Argument<Vec<String>> = serde_json::from_str(json_str).unwrap();
209 match arg {
210 Argument::Value(v) => assert_eq!(v, vec!["alice", "bob"]),
211 Argument::Ref(_) => panic!("expected Value variant"),
212 }
213 }
214
215 // Oracle: tests/fixtures/rfc8620-result-reference.json (RFC 8620 §9)
216 #[test]
217 fn result_reference_deserializes_from_fixture() {
218 let raw = include_str!("../tests/fixtures/rfc8620-result-reference.json");
219 let rr: ResultReference = serde_json::from_str(raw).expect("deserialize ResultReference");
220 assert_eq!(rr.result_of, "t0");
221 assert_eq!(rr.name, "Foo/changes");
222 assert_eq!(rr.path, "/created");
223 }
224
225 // Oracle: #[serde(untagged)] — ResultReference JSON object deserializes as Argument::Ref.
226 #[test]
227 fn argument_ref_deserializes() {
228 let j = r#"{"resultOf":"0","name":"X/get","path":"/ids"}"#;
229 let arg: Argument<Vec<String>> = serde_json::from_str(j).expect("deser");
230 match arg {
231 Argument::Ref(rr) => {
232 assert_eq!(rr.result_of, "0");
233 assert_eq!(rr.name, "X/get");
234 }
235 Argument::Value(_) => panic!("expected Ref"),
236 }
237 }
238
239 // Oracle: bd:JMAP-6xs8.27 — the sealed set now includes Date, UTCDate,
240 // and State (plus their Vec forms). These are #[serde(transparent)]
241 // String newtypes that satisfy the Argument<T> invariant (they
242 // deserialize as JSON strings, never as objects), so they are safe
243 // to use as the inner type of an Argument. This test exercises both
244 // variants for each, ensuring the sealed-set additions actually
245 // compile and round-trip.
246 #[test]
247 fn argument_string_newtypes_compile_and_round_trip() {
248 use crate::{Date, State, UTCDate};
249
250 // Date
251 let arg_date: Argument<Date> = Argument::Value(Date::from("2024-06-15"));
252 let j_date = serde_json::to_string(&arg_date).expect("serialize Date Value");
253 assert_eq!(j_date, "\"2024-06-15\"");
254 let back_date: Argument<Date> =
255 serde_json::from_str(&j_date).expect("deserialize Date Value");
256 assert_eq!(arg_date, back_date);
257
258 let arg_date_list: Argument<Vec<Date>> = Argument::Value(vec![Date::from("2024-06-15")]);
259 let _ = serde_json::to_string(&arg_date_list).expect("Vec<Date> Value");
260
261 // UTCDate
262 let arg_utc: Argument<UTCDate> = Argument::Value(UTCDate::from("2024-06-15T09:00:00Z"));
263 let j_utc = serde_json::to_string(&arg_utc).expect("serialize UTCDate Value");
264 assert_eq!(j_utc, "\"2024-06-15T09:00:00Z\"");
265 let back_utc: Argument<UTCDate> =
266 serde_json::from_str(&j_utc).expect("deserialize UTCDate Value");
267 assert_eq!(arg_utc, back_utc);
268
269 let arg_utc_list: Argument<Vec<UTCDate>> = Argument::Value(vec![]);
270 let _ = serde_json::to_string(&arg_utc_list).expect("Vec<UTCDate> Value");
271
272 // State
273 let arg_state: Argument<State> = Argument::Value(State::from("s-42"));
274 let j_state = serde_json::to_string(&arg_state).expect("serialize State Value");
275 assert_eq!(j_state, "\"s-42\"");
276 let back_state: Argument<State> =
277 serde_json::from_str(&j_state).expect("deserialize State Value");
278 assert_eq!(arg_state, back_state);
279
280 let arg_state_list: Argument<Vec<State>> = Argument::Value(vec![]);
281 let _ = serde_json::to_string(&arg_state_list).expect("Vec<State> Value");
282
283 // The Ref variant still works for the newly-added inner types.
284 let rr = ResultReference::new("0", "Email/changes", "/newState");
285 let arg_state_ref: Argument<State> = Argument::Ref(rr.clone());
286 let j_state_ref = serde_json::to_string(&arg_state_ref).expect("serialize Ref");
287 assert!(j_state_ref.contains("\"resultOf\""));
288 assert!(j_state_ref.contains("\"Email/changes\""));
289 let back_ref: Argument<State> =
290 serde_json::from_str(&j_state_ref).expect("deserialize Ref");
291 match back_ref {
292 Argument::Ref(rr2) => {
293 assert_eq!(rr2.result_of, "0");
294 assert_eq!(rr2.name, "Email/changes");
295 assert_eq!(rr2.path, "/newState");
296 }
297 Argument::Value(_) => panic!("expected Ref"),
298 }
299 }
300}