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 ids_slice = ids.as_deref();
41 let (list, not_found) = backend
42 .get_objects::<O>(&account_id, ids_slice, None)
43 .await
44 .map_err(|e| JmapError::server_fail(e.to_string()))?;
45
46 let state = backend
47 .get_state::<O>(&account_id)
48 .await
49 .map_err(|e| JmapError::server_fail(e.to_string()))?;
50
51 let list_json: Vec<Value> = list.iter().map(ser).collect::<Result<Vec<_>, _>>()?;
52
53 Ok((
54 json!({
55 "accountId": account_id.as_ref(),
56 "state": state.as_ref(),
57 "list": list_json,
58 "notFound": not_found_json(¬_found),
59 }),
60 vec![],
61 ))
62}
63
64pub async fn handle_changes<O: JmapObject, B: JmapBackend>(
73 backend: &B,
74 args: Value,
75) -> Result<(Value, Vec<Invocation>), JmapError> {
76 let account_id = extract_account_id(&args)?;
77 let Value::Object(args) = args else {
78 return Err(JmapError::invalid_arguments(
79 "arguments must be a JSON object",
80 ));
81 };
82
83 let since_state: State = match args.get("sinceState").and_then(|v| v.as_str()) {
84 Some(s) => State::from(s),
85 None => return Err(JmapError::invalid_arguments("sinceState is required")),
86 };
87
88 let max_changes: Option<u64> = match args.get("maxChanges") {
89 None | Some(Value::Null) => None,
90 Some(v) => Some(v.as_u64().filter(|&n| n > 0).ok_or_else(|| {
91 JmapError::invalid_arguments("maxChanges must be a positive integer")
92 })?),
93 };
94
95 let result = backend
96 .get_changes::<O>(&account_id, &since_state, max_changes)
97 .await
98 .map_err(JmapError::from)?;
99
100 Ok((
101 json!({
102 "accountId": account_id.as_ref(),
103 "oldState": since_state.as_ref(),
104 "newState": result.new_state.as_ref(),
105 "hasMoreChanges": result.has_more_changes,
106 "updatedProperties": Value::Null,
107 "created": result.created.iter().map(|id| id.as_ref()).collect::<Vec<_>>(),
108 "updated": result.updated.iter().map(|id| id.as_ref()).collect::<Vec<_>>(),
109 "destroyed": result.destroyed.iter().map(|id| id.as_ref()).collect::<Vec<_>>(),
110 }),
111 vec![],
112 ))
113}
114
115pub async fn handle_query<O: QueryObject, B: JmapBackend>(
124 backend: &B,
125 args: Value,
126) -> Result<(Value, Vec<Invocation>), JmapError> {
127 let account_id = extract_account_id(&args)?;
128 let Value::Object(mut args) = args else {
129 return Err(JmapError::invalid_arguments(
130 "arguments must be a JSON object",
131 ));
132 };
133
134 let calculate_total: bool = args
135 .get("calculateTotal")
136 .and_then(|v| v.as_bool())
137 .unwrap_or(false);
138
139 let limit: Option<u64> = match args.get("limit") {
140 None | Some(Value::Null) => None,
141 Some(v) => match v.as_u64() {
142 Some(n) => Some(n),
143 None => {
144 return Err(JmapError::invalid_arguments(format!(
145 "limit: expected a non-negative integer, got {v}"
146 )))
147 }
148 },
149 };
150
151 let position: i64 = match args.get("position") {
152 None | Some(Value::Null) => 0,
153 Some(v) => v.as_i64().ok_or_else(|| {
154 JmapError::invalid_arguments(format!("position: expected an integer, got {v}"))
155 })?,
156 };
157
158 let filter: Option<O::Filter> = match args.remove("filter").unwrap_or(Value::Null) {
159 Value::Null => None,
160 v => Some(serde_json::from_value(v).map_err(|_| JmapError::unsupported_filter())?),
161 };
162
163 let sort: Option<Vec<O::Comparator>> = match args.remove("sort").unwrap_or(Value::Null) {
164 Value::Null => None,
165 v => Some(
166 serde_json::from_value(v)
167 .map_err(|_| JmapError::invalid_arguments("sort must be an array"))?,
168 ),
169 };
170
171 let result = backend
172 .query_objects::<O>(
173 &account_id,
174 filter.as_ref(),
175 sort.as_deref(),
176 limit,
177 position,
178 )
179 .await
180 .map_err(|e| JmapError::server_fail(e.to_string()))?;
181
182 let mut resp = json!({
183 "accountId": account_id.as_ref(),
184 "queryState": result.query_state.as_ref(),
185 "canCalculateChanges": result.can_calculate_changes,
186 "position": result.position,
187 "ids": result.ids.iter().map(|id| id.as_ref()).collect::<Vec<_>>(),
188 });
189 if calculate_total {
190 if let Some(t) = result.total {
191 resp["total"] = json!(t);
192 }
193 }
194
195 Ok((resp, vec![]))
196}
197
198pub async fn handle_query_changes<O: QueryObject, B: JmapBackend>(
209 backend: &B,
210 args: Value,
211) -> Result<(Value, Vec<Invocation>), JmapError> {
212 let account_id = extract_account_id(&args)?;
213 let Value::Object(mut args) = args else {
214 return Err(JmapError::invalid_arguments(
215 "arguments must be a JSON object",
216 ));
217 };
218
219 let since_query_state: State = match args.get("sinceQueryState").and_then(|v| v.as_str()) {
220 Some(s) => State::from(s),
221 None => return Err(JmapError::invalid_arguments("sinceQueryState is required")),
222 };
223
224 let max_changes: Option<u64> = match args.get("maxChanges") {
225 None | Some(Value::Null) => None,
226 Some(v) => Some(v.as_u64().filter(|&n| n > 0).ok_or_else(|| {
227 JmapError::invalid_arguments("maxChanges must be a positive integer")
228 })?),
229 };
230
231 let up_to_id: Option<Id> = match args.get("upToId") {
232 None | Some(Value::Null) => None,
233 Some(Value::String(s)) => Some(Id::from(s.as_str())),
234 Some(_) => {
235 return Err(JmapError::invalid_arguments(
236 "upToId must be a string Id or null",
237 ))
238 }
239 };
240
241 let calculate_total: bool = args
242 .get("calculateTotal")
243 .and_then(|v| v.as_bool())
244 .unwrap_or(false);
245
246 let filter: Option<O::Filter> = match args.remove("filter").unwrap_or(Value::Null) {
247 Value::Null => None,
248 v => Some(serde_json::from_value(v).map_err(|_| JmapError::unsupported_filter())?),
249 };
250
251 let sort: Option<Vec<O::Comparator>> = match args.remove("sort").unwrap_or(Value::Null) {
252 Value::Null => None,
253 v => Some(
254 serde_json::from_value(v)
255 .map_err(|_| JmapError::invalid_arguments("sort must be an array"))?,
256 ),
257 };
258
259 let result = backend
260 .query_changes::<O>(
261 &account_id,
262 &since_query_state,
263 filter.as_ref(),
264 sort.as_deref(),
265 max_changes,
266 up_to_id.as_ref(),
267 false, )
269 .await
270 .map_err(JmapError::from)?;
271
272 let removed: Vec<&str> = result.removed.iter().map(|id| id.as_ref()).collect();
273 let added: Vec<Value> = result
274 .added
275 .iter()
276 .map(|item| {
277 json!({
278 "id": item.id.as_ref(),
279 "index": item.index,
280 })
281 })
282 .collect();
283
284 let mut resp = json!({
285 "accountId": account_id.as_ref(),
286 "oldQueryState": result.old_query_state.as_ref(),
287 "newQueryState": result.new_query_state.as_ref(),
288 "removed": removed,
289 "added": added,
290 });
291 if calculate_total {
292 if let Some(t) = result.total {
293 resp["total"] = json!(t);
294 }
295 }
296
297 Ok((resp, vec![]))
298}