1use std::fmt;
2
3use revision::revisioned;
4use surrealdb_strand::Strand;
5use surrealdb_types::{SqlFormat, ToSql};
6
7use crate::expr::part::Part;
8use crate::val::{Array, Object, Value};
9
10#[derive(Debug)]
11pub struct PatchError {
12 pub message: String,
13}
14
15impl fmt::Display for PatchError {
16 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17 write!(f, "Failed to parse JSON patch structure: {}", self.message.to_sql())
18 }
19}
20
21#[revisioned(revision = 1)]
24#[derive(Clone, Debug, Eq, PartialEq, Hash)]
25pub enum Operation {
26 Add {
27 path: Vec<Strand>,
28 value: Value,
29 },
30 Remove {
31 path: Vec<Strand>,
32 },
33 Replace {
34 path: Vec<Strand>,
35 value: Value,
36 },
37 Change {
38 path: Vec<Strand>,
39 value: Value,
40 },
41 Copy {
42 path: Vec<Strand>,
43 from: Vec<Strand>,
44 },
45 Move {
46 path: Vec<Strand>,
47 from: Vec<Strand>,
48 },
49 Test {
50 path: Vec<Strand>,
51 value: Value,
52 },
53}
54
55fn reject_root_from(op: &str, from: Vec<Strand>) -> Result<Vec<Strand>, PatchError> {
60 if from.is_empty() {
61 Err(PatchError {
62 message: format!("'{op}' operation requires a non-root 'from' pointer"),
63 })
64 } else {
65 Ok(from)
66 }
67}
68
69fn reject_nested_destination(op: &str, path: &[Strand], from: &[Strand]) -> Result<(), PatchError> {
82 if path.len() > from.len() && path.starts_with(from) {
83 return Err(PatchError {
84 message: format!("'{op}' operation requires a 'path' that is not inside 'from'"),
85 });
86 }
87 Ok(())
88}
89
90impl Operation {
91 pub fn max_path_len(&self) -> usize {
96 match self {
97 Operation::Add {
98 path,
99 ..
100 }
101 | Operation::Remove {
102 path,
103 }
104 | Operation::Replace {
105 path,
106 ..
107 }
108 | Operation::Change {
109 path,
110 ..
111 }
112 | Operation::Test {
113 path,
114 ..
115 } => path.len(),
116 Operation::Copy {
117 path,
118 from,
119 }
120 | Operation::Move {
121 path,
122 from,
123 } => path.len().max(from.len()),
124 }
125 }
126
127 pub fn array_index(token: &str) -> Option<usize> {
136 let index: usize = token.parse().ok()?;
137 (index.to_string() == token).then_some(index)
138 }
139
140 pub fn path_to_parts(path: &[Strand]) -> Vec<Part> {
156 path.iter()
157 .map(|p| match Self::array_index(p.as_str()).and_then(|i| i64::try_from(i).ok()) {
158 Some(i) => Part::index_int(i),
159 None => Part::Field(p.clone()),
160 })
161 .collect()
162 }
163
164 fn value_to_jsonpath(val: &Value) -> Vec<Strand> {
166 let raw = val.to_raw_string();
170 let trimmed = raw.trim_start_matches('/');
171 if trimmed.is_empty() {
172 Vec::new()
173 } else {
174 trimmed.split(&['.', '/']).map(Strand::from).collect()
175 }
176 }
177
178 pub fn into_object(self) -> Object {
180 fn path_to_jsonpath(p: &[Strand]) -> Value {
182 let mut res = String::with_capacity(p.len() + p.iter().map(|x| x.len()).sum::<usize>());
183 for p in p {
184 res.push('/');
185 res.push_str(p.as_str());
186 }
187 res.into()
188 }
189 Object(match self {
191 Operation::Add {
192 path,
193 value,
194 } => {
195 map! {
196 "op".into() => Value::from("add"),
197 "path".into() => path_to_jsonpath(&path),
198 "value".into() => value,
199 }
200 }
201 Operation::Remove {
202 path,
203 } => {
204 map! {
205 "op".into() => Value::from("remove"),
206 "path".into() => path_to_jsonpath(&path),
207 }
208 }
209 Operation::Replace {
210 path,
211 value,
212 } => {
213 map! {
214 "op".into() => Value::from("replace"),
215 "path".into() => path_to_jsonpath(&path),
216 "value".into() => value,
217 }
218 }
219 Operation::Change {
220 path,
221 value,
222 } => {
223 map! {
224 "op".into() => Value::from("change"),
225 "path".into() => path_to_jsonpath(&path),
226 "value".into() => value,
227 }
228 }
229 Operation::Copy {
230 path,
231 from,
232 } => {
233 map! {
234 "op".into() => Value::from("copy"),
235 "path".into() => path_to_jsonpath(&path),
236 "from".into() => path_to_jsonpath(&from),
237 }
238 }
239 Operation::Move {
240 path,
241 from,
242 } => {
243 map! {
244 "op".into() => Value::from("move"),
245 "path".into() => path_to_jsonpath(&path),
246 "from".into() => path_to_jsonpath(&from),
247 }
248 }
249 Operation::Test {
250 path,
251 value,
252 } => {
253 map! {
254 "op".into() => Value::from("test"),
255 "path".into() => path_to_jsonpath(&path),
256 "value".into() => value,
257 }
258 }
259 })
260 }
261
262 pub fn operation_from_object(object: &Object) -> Result<Operation, PatchError> {
265 let Some(op) = object.get("op") else {
266 return Err(PatchError {
267 message: "Key 'op' missing".to_owned(),
268 });
269 };
270
271 let Value::String(op) = op else {
272 return Err(PatchError {
273 message: "Key 'op' not a string".to_owned(),
274 });
275 };
276
277 let Some(path) = object.get("path") else {
278 return Err(PatchError {
279 message: "Key 'path' missing".to_owned(),
280 });
281 };
282
283 let from = || {
284 object.get("from").map(Operation::value_to_jsonpath).ok_or_else(|| PatchError {
285 message: "Key 'from' missing".to_owned(),
286 })
287 };
288
289 let value = || {
290 object.get("value").cloned().ok_or_else(|| PatchError {
291 message: "Key 'value' missing".to_owned(),
292 })
293 };
294
295 let path = Operation::value_to_jsonpath(path);
296
297 match op.as_str() {
298 "add" => Ok(Operation::Add {
299 path,
300 value: value()?,
301 }),
302 "remove" => Ok(Operation::Remove {
303 path,
304 }),
305 "replace" => Ok(Operation::Replace {
306 path,
307 value: value()?,
308 }),
309 "change" => Ok(Operation::Change {
310 path,
311 value: value()?,
312 }),
313 "copy" => {
314 let from = reject_root_from("copy", from()?)?;
315 reject_nested_destination("copy", &path, &from)?;
316 Ok(Operation::Copy {
317 path,
318 from,
319 })
320 }
321 "move" => {
322 let from = reject_root_from("move", from()?)?;
323 reject_nested_destination("move", &path, &from)?;
324 Ok(Operation::Move {
325 path,
326 from,
327 })
328 }
329 "test" => Ok(Operation::Test {
330 path,
331 value: value()?,
332 }),
333
334 x => Err(PatchError {
335 message: format!("Invalid operation '{x}'"),
336 }),
337 }
338 }
339
340 pub fn value_to_operations(value: Value) -> Result<Vec<Operation>, PatchError> {
343 let Value::Array(array) = value else {
344 return Err(PatchError {
345 message: "Patch operations should be an array of objects".to_owned(),
346 });
347 };
348
349 let mut res = Vec::new();
350 for o in array {
351 let Value::Object(o) = o else {
352 return Err(PatchError {
353 message: "Patch operations should be an array of objects".to_owned(),
354 });
355 };
356 res.push(Operation::operation_from_object(&o)?)
357 }
358 Ok(res)
359 }
360
361 pub fn operations_to_value(operations: Vec<Operation>) -> Value {
362 let array = operations.into_iter().map(|x| Value::Object(x.into_object())).collect();
363 Value::Array(Array(array))
364 }
365}
366
367impl ToSql for Operation {
368 fn fmt_sql(&self, f: &mut String, fmt: SqlFormat) {
369 self.clone().into_object().fmt_sql(f, fmt);
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376
377 fn roundtrip(op: &Operation) {
378 let obj = op.clone().into_object();
379 let decoded = Operation::operation_from_object(&obj)
380 .expect("operation_from_object should accept into_object output");
381 assert_eq!(*op, decoded);
382 }
383
384 #[test]
385 fn round_trip_all_variants() {
386 let path: Vec<Strand> = vec!["a".into(), "b".into()];
387 let from: Vec<Strand> = vec!["c".into(), "d".into()];
388 let value = Value::Bool(true);
389
390 roundtrip(&Operation::Add {
391 path: path.clone(),
392 value: value.clone(),
393 });
394 roundtrip(&Operation::Remove {
395 path: path.clone(),
396 });
397 roundtrip(&Operation::Replace {
398 path: path.clone(),
399 value: value.clone(),
400 });
401 roundtrip(&Operation::Change {
402 path: path.clone(),
403 value: value.clone(),
404 });
405 roundtrip(&Operation::Copy {
406 path: path.clone(),
407 from: from.clone(),
408 });
409 roundtrip(&Operation::Move {
410 path: path.clone(),
411 from,
412 });
413 roundtrip(&Operation::Test {
414 path,
415 value,
416 });
417 }
418
419 #[test]
420 fn round_trip_root_path() {
421 let empty: Vec<Strand> = Vec::new();
424 let value = Value::from("Hello there!");
425
426 roundtrip(&Operation::Add {
427 path: empty.clone(),
428 value: value.clone(),
429 });
430 roundtrip(&Operation::Remove {
431 path: empty.clone(),
432 });
433 roundtrip(&Operation::Replace {
434 path: empty.clone(),
435 value: value.clone(),
436 });
437 roundtrip(&Operation::Change {
438 path: empty.clone(),
439 value: value.clone(),
440 });
441 roundtrip(&Operation::Test {
442 path: empty,
443 value,
444 });
445 }
446}