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///
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}