jmap_types/backend.rs
1//! Backend marker traits shared across all JMAP server crates.
2//!
3//! These traits are defined here in `jmap-types` so that implementations can
4//! go in the type crates (`jmap-mail-types`, `jmap-chat-types`) without
5//! violating the orphan rule.
6
7// ---------------------------------------------------------------------------
8// Marker traits
9// ---------------------------------------------------------------------------
10
11/// Marker trait for all JMAP object types.
12///
13/// Implement this in the *types* crate for each first-class JMAP object (e.g.
14/// `Mailbox`, `Email`, `Chat`). The [`TYPE_NAME`](JmapObject::TYPE_NAME) constant
15/// is used in error messages and capability checks in the server crates.
16pub trait JmapObject:
17 serde::Serialize + for<'de> serde::Deserialize<'de> + Send + Sync + 'static
18{
19 /// The JMAP type name string (e.g. `"Email"`, `"Mailbox"`, `"Chat"`).
20 const TYPE_NAME: &'static str;
21 /// The property selector enum for this type (server-side only, no serde).
22 type Property: Send + Sync + 'static;
23}
24
25/// Marker for object types that support `get` and `changes` operations.
26pub trait GetObject: JmapObject {}
27
28/// Marker for object types that support `set` (create/update/destroy) operations.
29pub trait SetObject: JmapObject {
30 /// The patch type for update operations.
31 ///
32 /// Typically [`serde_json::Value`] holding an RFC 8620 §5.3 PatchObject,
33 /// whose keys are JSON Pointer paths (RFC 6901) with an implicit leading
34 /// `/`. This is not an RFC 7396 JSON Merge Patch. A typed struct is also
35 /// allowed if it serializes to that PatchObject shape.
36 type Patch: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static;
37
38 /// Wire names of the properties the spec marks "server-set": "Only the
39 /// server can set the value for this property. The client MUST NOT send
40 /// this property when creating a new object of this type." (RFC 8620
41 /// §1.1). A property the type's `/set` method text never accepts from a
42 /// client, on create or update, is server-set too.
43 ///
44 /// A `/set` create that carries one, or an update patch that changes
45 /// one, is `invalidProperties` (RFC 8620 §5.3).
46 const SERVER_SET: &'static [&'static str];
47
48 /// Wire names of every property that is not [`SERVER_SET`]: the ones a
49 /// client may set through `/set`.
50 ///
51 /// Each property of the type is in exactly one of [`SERVER_SET`] and
52 /// `CLIENT_SETTABLE`; [`check_property_classification`] tests that.
53 ///
54 /// [`SERVER_SET`]: SetObject::SERVER_SET
55 const CLIENT_SETTABLE: &'static [&'static str];
56
57 /// The [`CLIENT_SETTABLE`] properties the spec marks "immutable": "The
58 /// value MUST NOT change after the object is created." (RFC 8620 §1.1).
59 /// A property the `/set` method text accepts on create but not on update
60 /// is immutable too.
61 ///
62 /// A client may set one in a `/set` create; an update patch that changes
63 /// one is `invalidProperties`.
64 ///
65 /// [`CLIENT_SETTABLE`]: SetObject::CLIENT_SETTABLE
66 const IMMUTABLE: &'static [&'static str];
67}
68
69/// Check the property classification of `T` against `obj`, a value with
70/// every field populated.
71///
72/// Every top-level property `obj` serializes MUST be in exactly one of
73/// [`SetObject::SERVER_SET`] and [`SetObject::CLIENT_SETTABLE`], and every
74/// [`SetObject::IMMUTABLE`] name MUST be in `CLIENT_SETTABLE`.
75///
76/// Types crates call this from a test that builds `obj` with a struct
77/// literal and no `..Default::default()`. A field added to the struct then
78/// fails to compile until the test sets it, and fails this check until it is
79/// classified.
80///
81/// # Errors
82///
83/// Returns every problem found, one per line, or a single line if `obj`
84/// does not serialize to a JSON object.
85pub fn check_property_classification<T: SetObject>(obj: &T) -> Result<(), String> {
86 let value = serde_json::to_value(obj).map_err(|e| format!("serialize: {e}"))?;
87 let serde_json::Value::Object(map) = value else {
88 return Err(format!("{} does not serialize to an object", T::TYPE_NAME));
89 };
90 let mut problems = Vec::new();
91 for key in map.keys() {
92 let k = key.as_str();
93 match (T::SERVER_SET.contains(&k), T::CLIENT_SETTABLE.contains(&k)) {
94 (false, false) => problems.push(format!("{}.{k} is unclassified", T::TYPE_NAME)),
95 (true, true) => problems.push(format!("{}.{k} is classified twice", T::TYPE_NAME)),
96 _ => {}
97 }
98 }
99 for name in T::IMMUTABLE {
100 if !T::CLIENT_SETTABLE.contains(name) {
101 problems.push(format!(
102 "{}.{name} is IMMUTABLE but not CLIENT_SETTABLE",
103 T::TYPE_NAME
104 ));
105 }
106 }
107 if problems.is_empty() {
108 Ok(())
109 } else {
110 Err(problems.join("\n"))
111 }
112}
113
114/// Marker for object types that support `query` and `queryChanges` operations.
115pub trait QueryObject: JmapObject {
116 /// The filter condition type.
117 ///
118 /// Must implement both `Serialize` and `DeserializeOwned` so that backends
119 /// can inspect or forward the filter.
120 type Filter: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static;
121 /// The comparator type for sort operations.
122 type Comparator: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static;
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[derive(serde::Serialize, serde::Deserialize)]
130 struct Probe {
131 id: String,
132 name: String,
133 email: String,
134 }
135
136 impl JmapObject for Probe {
137 const TYPE_NAME: &'static str = "Probe";
138 type Property = ();
139 }
140
141 impl SetObject for Probe {
142 type Patch = serde_json::Value;
143 const SERVER_SET: &'static [&'static str] = &["id"];
144 const CLIENT_SETTABLE: &'static [&'static str] = &["name", "email"];
145 const IMMUTABLE: &'static [&'static str] = &["email"];
146 }
147
148 /// The same shape with `email` left out of both lists, `name` in both,
149 /// and an IMMUTABLE name that is server-set.
150 #[derive(serde::Serialize, serde::Deserialize)]
151 struct BadProbe {
152 id: String,
153 name: String,
154 email: String,
155 }
156
157 impl JmapObject for BadProbe {
158 const TYPE_NAME: &'static str = "BadProbe";
159 type Property = ();
160 }
161
162 impl SetObject for BadProbe {
163 type Patch = serde_json::Value;
164 const SERVER_SET: &'static [&'static str] = &["id", "name"];
165 const CLIENT_SETTABLE: &'static [&'static str] = &["name"];
166 const IMMUTABLE: &'static [&'static str] = &["id"];
167 }
168
169 fn probe<T: SetObject>(make: fn(String, String, String) -> T) -> Result<(), String> {
170 check_property_classification(&make("i".into(), "n".into(), "e".into()))
171 }
172
173 #[test]
174 fn classification_accepts_a_partition() {
175 assert_eq!(probe(|id, name, email| Probe { id, name, email }), Ok(()));
176 }
177
178 #[test]
179 fn classification_reports_every_problem() {
180 let err = probe(|id, name, email| BadProbe { id, name, email }).unwrap_err();
181 let mut lines: Vec<&str> = err.lines().collect();
182 lines.sort_unstable();
183 assert_eq!(
184 lines,
185 [
186 "BadProbe.email is unclassified",
187 "BadProbe.id is IMMUTABLE but not CLIENT_SETTABLE",
188 "BadProbe.name is classified twice",
189 ]
190 );
191 }
192}