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 array_index(token: &str) -> Option<usize> {
100 let index: usize = token.parse().ok()?;
101 (index.to_string() == token).then_some(index)
102 }
103
104 pub fn path_to_parts(path: &[Strand]) -> Vec<Part> {
120 path.iter()
121 .map(|p| match Self::array_index(p.as_str()).and_then(|i| i64::try_from(i).ok()) {
122 Some(i) => Part::index_int(i),
123 None => Part::Field(p.clone()),
124 })
125 .collect()
126 }
127
128 fn value_to_jsonpath(val: &Value) -> Vec<Strand> {
130 let raw = val.to_raw_string();
134 let trimmed = raw.trim_start_matches('/');
135 if trimmed.is_empty() {
136 Vec::new()
137 } else {
138 trimmed.split(&['.', '/']).map(Strand::from).collect()
139 }
140 }
141
142 pub fn into_object(self) -> Object {
144 fn path_to_jsonpath(p: &[Strand]) -> Value {
146 let mut res = String::with_capacity(p.len() + p.iter().map(|x| x.len()).sum::<usize>());
147 for p in p {
148 res.push('/');
149 res.push_str(p.as_str());
150 }
151 res.into()
152 }
153 Object(match self {
155 Operation::Add {
156 path,
157 value,
158 } => {
159 map! {
160 "op".into() => Value::from("add"),
161 "path".into() => path_to_jsonpath(&path),
162 "value".into() => value,
163 }
164 }
165 Operation::Remove {
166 path,
167 } => {
168 map! {
169 "op".into() => Value::from("remove"),
170 "path".into() => path_to_jsonpath(&path),
171 }
172 }
173 Operation::Replace {
174 path,
175 value,
176 } => {
177 map! {
178 "op".into() => Value::from("replace"),
179 "path".into() => path_to_jsonpath(&path),
180 "value".into() => value,
181 }
182 }
183 Operation::Change {
184 path,
185 value,
186 } => {
187 map! {
188 "op".into() => Value::from("change"),
189 "path".into() => path_to_jsonpath(&path),
190 "value".into() => value,
191 }
192 }
193 Operation::Copy {
194 path,
195 from,
196 } => {
197 map! {
198 "op".into() => Value::from("copy"),
199 "path".into() => path_to_jsonpath(&path),
200 "from".into() => path_to_jsonpath(&from),
201 }
202 }
203 Operation::Move {
204 path,
205 from,
206 } => {
207 map! {
208 "op".into() => Value::from("move"),
209 "path".into() => path_to_jsonpath(&path),
210 "from".into() => path_to_jsonpath(&from),
211 }
212 }
213 Operation::Test {
214 path,
215 value,
216 } => {
217 map! {
218 "op".into() => Value::from("test"),
219 "path".into() => path_to_jsonpath(&path),
220 "value".into() => value,
221 }
222 }
223 })
224 }
225
226 pub fn operation_from_object(object: &Object) -> Result<Operation, PatchError> {
229 let Some(op) = object.get("op") else {
230 return Err(PatchError {
231 message: "Key 'op' missing".to_owned(),
232 });
233 };
234
235 let Value::String(op) = op else {
236 return Err(PatchError {
237 message: "Key 'op' not a string".to_owned(),
238 });
239 };
240
241 let Some(path) = object.get("path") else {
242 return Err(PatchError {
243 message: "Key 'path' missing".to_owned(),
244 });
245 };
246
247 let from = || {
248 object.get("from").map(Operation::value_to_jsonpath).ok_or_else(|| PatchError {
249 message: "Key 'from' missing".to_owned(),
250 })
251 };
252
253 let value = || {
254 object.get("value").cloned().ok_or_else(|| PatchError {
255 message: "Key 'value' missing".to_owned(),
256 })
257 };
258
259 let path = Operation::value_to_jsonpath(path);
260
261 match op.as_str() {
262 "add" => Ok(Operation::Add {
263 path,
264 value: value()?,
265 }),
266 "remove" => Ok(Operation::Remove {
267 path,
268 }),
269 "replace" => Ok(Operation::Replace {
270 path,
271 value: value()?,
272 }),
273 "change" => Ok(Operation::Change {
274 path,
275 value: value()?,
276 }),
277 "copy" => {
278 let from = reject_root_from("copy", from()?)?;
279 reject_nested_destination("copy", &path, &from)?;
280 Ok(Operation::Copy {
281 path,
282 from,
283 })
284 }
285 "move" => {
286 let from = reject_root_from("move", from()?)?;
287 reject_nested_destination("move", &path, &from)?;
288 Ok(Operation::Move {
289 path,
290 from,
291 })
292 }
293 "test" => Ok(Operation::Test {
294 path,
295 value: value()?,
296 }),
297
298 x => Err(PatchError {
299 message: format!("Invalid operation '{x}'"),
300 }),
301 }
302 }
303
304 pub fn value_to_operations(value: Value) -> Result<Vec<Operation>, PatchError> {
307 let Value::Array(array) = value else {
308 return Err(PatchError {
309 message: "Patch operations should be an array of objects".to_owned(),
310 });
311 };
312
313 let mut res = Vec::new();
314 for o in array {
315 let Value::Object(o) = o else {
316 return Err(PatchError {
317 message: "Patch operations should be an array of objects".to_owned(),
318 });
319 };
320 res.push(Operation::operation_from_object(&o)?)
321 }
322 Ok(res)
323 }
324
325 pub fn operations_to_value(operations: Vec<Operation>) -> Value {
326 let array = operations.into_iter().map(|x| Value::Object(x.into_object())).collect();
327 Value::Array(Array(array))
328 }
329}
330
331impl ToSql for Operation {
332 fn fmt_sql(&self, f: &mut String, fmt: SqlFormat) {
333 self.clone().into_object().fmt_sql(f, fmt);
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340
341 fn roundtrip(op: &Operation) {
342 let obj = op.clone().into_object();
343 let decoded = Operation::operation_from_object(&obj)
344 .expect("operation_from_object should accept into_object output");
345 assert_eq!(*op, decoded);
346 }
347
348 #[test]
349 fn round_trip_all_variants() {
350 let path: Vec<Strand> = vec!["a".into(), "b".into()];
351 let from: Vec<Strand> = vec!["c".into(), "d".into()];
352 let value = Value::Bool(true);
353
354 roundtrip(&Operation::Add {
355 path: path.clone(),
356 value: value.clone(),
357 });
358 roundtrip(&Operation::Remove {
359 path: path.clone(),
360 });
361 roundtrip(&Operation::Replace {
362 path: path.clone(),
363 value: value.clone(),
364 });
365 roundtrip(&Operation::Change {
366 path: path.clone(),
367 value: value.clone(),
368 });
369 roundtrip(&Operation::Copy {
370 path: path.clone(),
371 from: from.clone(),
372 });
373 roundtrip(&Operation::Move {
374 path: path.clone(),
375 from,
376 });
377 roundtrip(&Operation::Test {
378 path,
379 value,
380 });
381 }
382
383 #[test]
384 fn round_trip_root_path() {
385 let empty: Vec<Strand> = Vec::new();
388 let value = Value::from("Hello there!");
389
390 roundtrip(&Operation::Add {
391 path: empty.clone(),
392 value: value.clone(),
393 });
394 roundtrip(&Operation::Remove {
395 path: empty.clone(),
396 });
397 roundtrip(&Operation::Replace {
398 path: empty.clone(),
399 value: value.clone(),
400 });
401 roundtrip(&Operation::Change {
402 path: empty.clone(),
403 value: value.clone(),
404 });
405 roundtrip(&Operation::Test {
406 path: empty,
407 value,
408 });
409 }
410}