1use jmap_types::{Id, Invocation, JmapError, State};
8use serde_json::{json, Value};
9
10use crate::backend::{GetObject, JmapBackend, JmapObject, QueryObject};
11use crate::helpers::{extract_account_id, not_found_json, ser};
12
13pub async fn handle_get<O: GetObject, B: JmapBackend>(
22 backend: &B,
23 args: Value,
24) -> Result<(Value, Vec<Invocation>), JmapError> {
25 let account_id = extract_account_id(&args)?;
26 let Value::Object(mut args) = args else {
27 return Err(JmapError::invalid_arguments(
28 "arguments must be a JSON object",
29 ));
30 };
31
32 let ids: Option<Vec<Id>> = match args.remove("ids").unwrap_or(Value::Null) {
33 Value::Null => None,
34 v => Some(
35 serde_json::from_value(v)
36 .map_err(|_| JmapError::invalid_arguments("ids must be an Id array"))?,
37 ),
38 };
39
40 let properties: Option<Vec<String>> = match args.remove("properties").unwrap_or(Value::Null) {
41 Value::Null => None,
42 v => Some(
43 serde_json::from_value(v)
44 .map_err(|_| JmapError::invalid_arguments("properties must be a string array"))?,
45 ),
46 };
47
48 let ids_slice = ids.as_deref();
49 let (list, not_found) = backend
50 .get_objects::<O>(&account_id, ids_slice, properties.as_deref())
51 .await
52 .map_err(|e| JmapError::server_fail(e.to_string()))?;
53
54 let state = backend
55 .get_state::<O>(&account_id)
56 .await
57 .map_err(|e| JmapError::server_fail(e.to_string()))?;
58
59 let list_json: Vec<Value> = list.iter().map(ser).collect::<Result<Vec<_>, _>>()?;
60
61 Ok((
62 json!({
63 "accountId": account_id.as_ref(),
64 "state": state.as_ref(),
65 "list": list_json,
66 "notFound": not_found_json(¬_found),
67 }),
68 vec![],
69 ))
70}
71
72pub async fn handle_changes<O: JmapObject, B: JmapBackend>(
81 backend: &B,
82 args: Value,
83) -> Result<(Value, Vec<Invocation>), JmapError> {
84 let account_id = extract_account_id(&args)?;
85 let Value::Object(args) = args else {
86 return Err(JmapError::invalid_arguments(
87 "arguments must be a JSON object",
88 ));
89 };
90
91 let since_state: State = match args.get("sinceState").and_then(|v| v.as_str()) {
92 Some(s) => State::from(s),
93 None => return Err(JmapError::invalid_arguments("sinceState is required")),
94 };
95
96 let max_changes: Option<u64> = match args.get("maxChanges") {
97 None | Some(Value::Null) => None,
98 Some(v) => Some(v.as_u64().filter(|&n| n > 0).ok_or_else(|| {
99 JmapError::invalid_arguments("maxChanges must be a positive integer")
100 })?),
101 };
102
103 let result = backend
104 .get_changes::<O>(&account_id, &since_state, max_changes)
105 .await
106 .map_err(JmapError::from)?;
107
108 Ok((
109 json!({
110 "accountId": account_id.as_ref(),
111 "oldState": since_state.as_ref(),
112 "newState": result.new_state.as_ref(),
113 "hasMoreChanges": result.has_more_changes,
114 "updatedProperties": Value::Null,
115 "created": result.created.iter().map(|id| id.as_ref()).collect::<Vec<_>>(),
116 "updated": result.updated.iter().map(|id| id.as_ref()).collect::<Vec<_>>(),
117 "destroyed": result.destroyed.iter().map(|id| id.as_ref()).collect::<Vec<_>>(),
118 }),
119 vec![],
120 ))
121}
122
123pub async fn handle_query<O: QueryObject, B: JmapBackend>(
132 backend: &B,
133 args: Value,
134) -> Result<(Value, Vec<Invocation>), JmapError> {
135 let account_id = extract_account_id(&args)?;
136 let Value::Object(mut args) = args else {
137 return Err(JmapError::invalid_arguments(
138 "arguments must be a JSON object",
139 ));
140 };
141
142 let calculate_total: bool = args
143 .get("calculateTotal")
144 .and_then(|v| v.as_bool())
145 .unwrap_or(false);
146
147 let limit: Option<u64> = match args.get("limit") {
148 None | Some(Value::Null) => None,
149 Some(v) => match v.as_u64() {
150 Some(n) => Some(n),
151 None => {
152 return Err(JmapError::invalid_arguments(format!(
153 "limit: expected a non-negative integer, got {v}"
154 )))
155 }
156 },
157 };
158
159 let position: i64 = match args.get("position") {
160 None | Some(Value::Null) => 0,
161 Some(v) => v.as_i64().ok_or_else(|| {
162 JmapError::invalid_arguments(format!("position: expected an integer, got {v}"))
163 })?,
164 };
165
166 let filter: Option<O::Filter> = match args.remove("filter").unwrap_or(Value::Null) {
167 Value::Null => None,
168 v => Some(serde_json::from_value(v).map_err(|_| JmapError::unsupported_filter())?),
169 };
170
171 let sort: Option<Vec<O::Comparator>> = match args.remove("sort").unwrap_or(Value::Null) {
172 Value::Null => None,
173 v => Some(
174 serde_json::from_value(v)
175 .map_err(|_| JmapError::invalid_arguments("sort must be an array"))?,
176 ),
177 };
178
179 let result = backend
180 .query_objects::<O>(
181 &account_id,
182 filter.as_ref(),
183 sort.as_deref(),
184 limit,
185 position,
186 )
187 .await
188 .map_err(|e| JmapError::server_fail(e.to_string()))?;
189
190 let mut resp = json!({
191 "accountId": account_id.as_ref(),
192 "queryState": result.query_state.as_ref(),
193 "canCalculateChanges": result.can_calculate_changes,
194 "position": result.position,
195 "ids": result.ids.iter().map(|id| id.as_ref()).collect::<Vec<_>>(),
196 });
197 if calculate_total {
198 if let Some(t) = result.total {
199 resp["total"] = json!(t);
200 }
201 }
202
203 Ok((resp, vec![]))
204}
205
206pub async fn handle_query_changes<O: QueryObject, B: JmapBackend>(
217 backend: &B,
218 args: Value,
219) -> Result<(Value, Vec<Invocation>), JmapError> {
220 let account_id = extract_account_id(&args)?;
221 let Value::Object(mut args) = args else {
222 return Err(JmapError::invalid_arguments(
223 "arguments must be a JSON object",
224 ));
225 };
226
227 let since_query_state: State = match args.get("sinceQueryState").and_then(|v| v.as_str()) {
228 Some(s) => State::from(s),
229 None => return Err(JmapError::invalid_arguments("sinceQueryState is required")),
230 };
231
232 let max_changes: Option<u64> = match args.get("maxChanges") {
233 None | Some(Value::Null) => None,
234 Some(v) => Some(v.as_u64().filter(|&n| n > 0).ok_or_else(|| {
235 JmapError::invalid_arguments("maxChanges must be a positive integer")
236 })?),
237 };
238
239 let up_to_id: Option<Id> = match args.get("upToId") {
240 None | Some(Value::Null) => None,
241 Some(Value::String(s)) => Some(Id::from(s.as_str())),
242 Some(_) => {
243 return Err(JmapError::invalid_arguments(
244 "upToId must be a string Id or null",
245 ))
246 }
247 };
248
249 let calculate_total: bool = args
250 .get("calculateTotal")
251 .and_then(|v| v.as_bool())
252 .unwrap_or(false);
253
254 let filter: Option<O::Filter> = match args.remove("filter").unwrap_or(Value::Null) {
255 Value::Null => None,
256 v => Some(serde_json::from_value(v).map_err(|_| JmapError::unsupported_filter())?),
257 };
258
259 let sort: Option<Vec<O::Comparator>> = match args.remove("sort").unwrap_or(Value::Null) {
260 Value::Null => None,
261 v => Some(
262 serde_json::from_value(v)
263 .map_err(|_| JmapError::invalid_arguments("sort must be an array"))?,
264 ),
265 };
266
267 let result = backend
268 .query_changes::<O>(
269 &account_id,
270 &since_query_state,
271 filter.as_ref(),
272 sort.as_deref(),
273 max_changes,
274 up_to_id.as_ref(),
275 false, )
277 .await
278 .map_err(JmapError::from)?;
279
280 let removed: Vec<&str> = result.removed.iter().map(|id| id.as_ref()).collect();
281 let added: Vec<Value> = result
282 .added
283 .iter()
284 .map(|item| {
285 json!({
286 "id": item.id.as_ref(),
287 "index": item.index,
288 })
289 })
290 .collect();
291
292 let mut resp = json!({
293 "accountId": account_id.as_ref(),
294 "oldQueryState": result.old_query_state.as_ref(),
295 "newQueryState": result.new_query_state.as_ref(),
296 "removed": removed,
297 "added": added,
298 });
299 if calculate_total {
300 if let Some(t) = result.total {
301 resp["total"] = json!(t);
302 }
303 }
304
305 Ok((resp, vec![]))
306}