1use indexmap::IndexMap;
15use parse_rust_core::op::OpPath;
16use parse_rust_core::{
17 classify_field, ErrorCode, FieldWrite, Op, ParseError, ParseMap, ParseValue,
18};
19use parse_rust_storage::{Update, UpdateValue};
20use serde_json::Value as Json;
21
22pub type WriteBody = IndexMap<String, FieldWrite>;
24
25pub fn decode_write_body(value: &Json, path: OpPath) -> Result<WriteBody, ParseError> {
31 let Json::Object(map) = value else {
32 return Err(ParseError::invalid_json("body must be an object"));
33 };
34 let mut out = WriteBody::new();
35 for (key, value) in map {
36 out.insert(key.clone(), classify_field(value.clone(), path)?);
37 }
38 Ok(out)
39}
40
41pub fn as_plain_body(body: &WriteBody) -> ParseMap {
48 let mut out = ParseMap::new();
49 for (key, write) in body {
50 let value = match write {
51 FieldWrite::Value(v) => v.clone(),
52 FieldWrite::Op(op) => {
53 let mut envelope = ParseMap::new();
54 envelope.insert(
55 "__op".to_string(),
56 ParseValue::String(op.name().to_string()),
57 );
58 ParseValue::Object(envelope)
59 }
60 };
61 out.insert(key.clone(), value);
62 }
63 out
64}
65
66pub fn flatten_for_create(body: &WriteBody) -> Result<ParseMap, ParseError> {
71 let mut out = ParseMap::new();
72 for (key, write) in body {
73 match write {
74 FieldWrite::Value(value) => {
75 out.insert(key.clone(), value.clone());
76 }
77 FieldWrite::Op(op) => {
78 if let Some(value) = op.flatten_for_create()? {
79 out.insert(key.clone(), value);
80 }
81 }
82 }
83 }
84 Ok(out)
85}
86
87pub fn lower_update(body: &WriteBody) -> Result<Update, ParseError> {
93 let mut out = Update::new();
94 for (key, write) in body {
95 let value = match write {
96 FieldWrite::Value(value) => UpdateValue::Set(value.clone()),
97 FieldWrite::Op(op) => match op {
98 Op::Increment(amount) => UpdateValue::Increment(*amount),
99 Op::SetOnInsert(value) => UpdateValue::SetOnInsert(value.clone()),
100 Op::Add(objects) => UpdateValue::Add(objects.clone()),
101 Op::AddUnique(objects) => UpdateValue::AddUnique(objects.clone()),
102 Op::Remove(objects) => UpdateValue::Remove(objects.clone()),
103 Op::Delete => UpdateValue::Unset,
104 Op::AddRelation(_) | Op::RemoveRelation(_) | Op::Batch(_) => {
105 return Err(ParseError::new(
106 ErrorCode::CommandUnavailable,
107 format!("The {} operator is not supported yet.", op.name()),
108 ))
109 }
110 },
111 };
112 out.insert(key.clone(), value);
113 }
114 Ok(out)
115}
116
117pub fn enforce_object_id_policy(
132 body: &WriteBody,
133 allow_custom_object_id: bool,
134) -> Result<(), ParseError> {
135 let present = |key: &str| match body.get(key) {
136 Some(FieldWrite::Value(value)) => Some(value),
137 Some(FieldWrite::Op(_)) => Some(&ParseValue::Bool(true)),
140 None => None,
141 };
142
143 if allow_custom_object_id {
144 if let Some(value) = present("objectId") {
145 if !parse_rust_core::is_js_truthy(value) {
146 return Err(ParseError::new(
147 ErrorCode::MissingObjectId,
148 "objectId must not be empty, null or undefined",
149 ));
150 }
151 }
152 return Ok(());
153 }
154
155 for key in ["objectId", "id"] {
156 if present(key).is_some_and(parse_rust_core::is_js_truthy) {
157 return Err(ParseError::new(
158 ErrorCode::InvalidKeyName,
159 format!("{key} is an invalid field name."),
160 ));
161 }
162 }
163 Ok(())
164}
165
166pub fn echoed_keys(body: &WriteBody) -> Vec<String> {
173 body.iter()
174 .filter_map(|(key, write)| match write {
175 FieldWrite::Op(op) if op.echoes_result() => Some(key.clone()),
176 _ => None,
177 })
178 .collect()
179}
180
181pub fn echo_response(body: &WriteBody, row: Option<&ParseMap>) -> ParseMap {
184 let mut out = ParseMap::new();
185 let Some(row) = row else { return out };
186 for key in echoed_keys(body) {
187 if let Some(value) = row.get(&key) {
188 out.insert(key, value.clone());
189 }
190 }
191 out
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 fn body(json: &str, path: OpPath) -> WriteBody {
199 decode_write_body(&serde_json::from_str(json).expect("test literal"), path).expect("decode")
200 }
201
202 #[test]
203 fn an_op_is_decoded_rather_than_stored_as_an_object() {
204 let b = body(
205 r#"{"views":{"__op":"Increment","amount":2}}"#,
206 OpPath::Update,
207 );
208 assert!(matches!(b.get("views"), Some(FieldWrite::Op(Op::Increment(a))) if *a == 2.0));
209 let update = lower_update(&b).expect("lower");
210 assert!(matches!(update.get("views"), Some(UpdateValue::Increment(a)) if *a == 2.0));
211 }
212
213 #[test]
214 fn create_flattens_every_op_the_way_upstream_does() {
215 let b = body(
216 r#"{
217 "views":{"__op":"Increment","amount":3},
218 "tags":{"__op":"Add","objects":["a"]},
219 "unique":{"__op":"AddUnique","objects":["b"]},
220 "gone":{"__op":"Remove","objects":["c"]},
221 "dropped":{"__op":"Delete"},
222 "plain":"x"
223 }"#,
224 OpPath::Create,
225 );
226 let row = flatten_for_create(&b).expect("flatten");
227 assert!(matches!(row.get("views"), Some(ParseValue::Number(n)) if *n == 3.0));
228 assert!(matches!(row.get("tags"), Some(ParseValue::Array(a)) if a.len() == 1));
229 assert!(matches!(row.get("unique"), Some(ParseValue::Array(a)) if a.len() == 1));
230 assert!(
231 matches!(row.get("gone"), Some(ParseValue::Array(a)) if a.is_empty()),
232 "Remove yields an empty array rather than removing anything"
233 );
234 assert!(row.get("dropped").is_none());
235 assert!(row.get("plain").is_some());
236 }
237
238 #[test]
239 fn only_the_five_result_bearing_ops_echo_back() {
240 let b = body(
241 r#"{
242 "views":{"__op":"Increment","amount":1},
243 "tags":{"__op":"Add","objects":["a"]},
244 "unique":{"__op":"AddUnique","objects":["b"]},
245 "gone":{"__op":"Remove","objects":["c"]},
246 "dropped":{"__op":"Delete"},
247 "plain":"x"
248 }"#,
249 OpPath::Update,
250 );
251 assert_eq!(echoed_keys(&b), vec!["views", "tags", "unique", "gone"]);
252
253 let mut row = ParseMap::new();
254 row.insert("views".into(), ParseValue::Number(4.0));
255 row.insert("plain".into(), ParseValue::String("x".into()));
256 let echoed = echo_response(&b, Some(&row));
257 assert!(matches!(echoed.get("views"), Some(ParseValue::Number(n)) if *n == 4.0));
258 assert!(
259 echoed.get("plain").is_none(),
260 "a plain set tells the client nothing it did not already know"
261 );
262 }
263
264 #[test]
265 fn the_increment_message_differs_by_path() {
266 let bad = serde_json::from_str(r#"{"n":{"__op":"Increment","amount":"x"}}"#)
267 .expect("test literal");
268 assert_eq!(
269 decode_write_body(&bad, OpPath::Create).unwrap_err().message,
270 "objects to add must be an array"
271 );
272 assert_eq!(
273 decode_write_body(&bad, OpPath::Update).unwrap_err().message,
274 "incrementing must provide a number"
275 );
276 }
277
278 #[test]
279 fn a_relation_op_reaching_the_update_lowering_is_refused() {
280 let b = body(
281 r#"{"users":{"__op":"AddRelation","objects":[]}}"#,
282 OpPath::Update,
283 );
284 let e = lower_update(&b).unwrap_err();
285 assert_eq!(e.code, ErrorCode::CommandUnavailable);
286 }
287
288 #[test]
289 fn a_delete_survives_as_its_envelope_in_the_plain_view() {
290 let b = body(r#"{"ACL":{"__op":"Delete"},"n":1}"#, OpPath::Update);
291 let plain = as_plain_body(&b);
292 match plain.get("ACL") {
293 Some(ParseValue::Object(map)) => {
294 assert!(matches!(map.get("__op"), Some(ParseValue::String(s)) if s == "Delete"))
295 }
296 other => panic!("expected an op envelope, got {other:?}"),
297 }
298 assert!(matches!(plain.get("n"), Some(ParseValue::Number(_))));
299 }
300
301 #[test]
302 fn the_default_refuses_a_client_supplied_object_id_and_id() {
303 for key in ["objectId", "id"] {
304 let b = body(
305 &format!(r#"{{"{key}":"chosen","title":"a"}}"#),
306 OpPath::Create,
307 );
308 let e = enforce_object_id_policy(&b, false).unwrap_err();
309 assert_eq!(e.code, ErrorCode::InvalidKeyName);
310 assert_eq!(e.message, format!("{key} is an invalid field name."));
311 }
312 let plain = body(r#"{"title":"a"}"#, OpPath::Create);
314 assert!(enforce_object_id_policy(&plain, false).is_ok());
315 }
316
317 #[test]
320 fn a_falsy_object_id_is_not_a_custom_one() {
321 let b = body(r#"{"objectId":""}"#, OpPath::Create);
322 assert!(enforce_object_id_policy(&b, false).is_ok());
323 }
324
325 #[test]
328 fn allowing_custom_ids_refuses_only_an_empty_one() {
329 let chosen = body(r#"{"objectId":"chosen"}"#, OpPath::Create);
330 assert!(enforce_object_id_policy(&chosen, true).is_ok());
331
332 for literal in [r#"{"objectId":""}"#, r#"{"objectId":null}"#] {
333 let b = body(literal, OpPath::Create);
334 let e = enforce_object_id_policy(&b, true).unwrap_err();
335 assert_eq!(e.code, ErrorCode::MissingObjectId);
336 assert_eq!(e.message, "objectId must not be empty, null or undefined");
337 }
338
339 let absent = body(r#"{"title":"a"}"#, OpPath::Create);
341 assert!(enforce_object_id_policy(&absent, true).is_ok());
342 }
343}