use anyhow::{Result, bail, ensure};
use surrealdb_types::ToSql;
use crate::expr::operation::PatchError;
use crate::expr::{Error, Operation};
use crate::val::{Set, Strand, Value};
#[derive(Clone, Copy)]
enum Write {
Insert,
Overwrite,
}
enum Located<'a> {
Found(&'a mut Value),
Absent,
Many,
}
fn locate<'a>(this: &'a mut Value, path: &[Strand]) -> Located<'a> {
let mut at = this;
for token in path {
at = match at {
Value::Object(o) => match o.get_mut(token.as_str()) {
Some(v) => v,
None => return Located::Absent,
},
Value::Array(a) => match Operation::array_index(token.as_str()) {
Some(i) => match a.get_mut(i) {
Some(v) => v,
None => return Located::Absent,
},
None => return Located::Many,
},
Value::Set(_) => return Located::Many,
_ => return Located::Absent,
};
}
Located::Found(at)
}
fn normalize_sets(value: &mut Value) {
match value {
Value::Set(s) => {
let mut items = Vec::from(std::mem::take(s));
items.iter_mut().for_each(normalize_sets);
*s = Set::from(items);
}
Value::Array(a) => a.iter_mut().for_each(normalize_sets),
Value::Object(o) => o.iter_mut().for_each(|(_, v)| normalize_sets(v)),
_ => (),
}
}
fn write(this: &mut Value, path: &[Strand], value: Value, mode: Write) -> Result<()> {
let Some((last, left)) = path.split_last() else {
*this = value;
return Ok(());
};
let position = match last.as_str() {
"-" => Some(None),
token => Operation::array_index(token).map(Some),
};
if let Some(index) = position {
match locate(this, left) {
Located::Found(Value::Array(v)) => {
let len = v.len();
let out_of_bounds = || {
Error::InvalidPatch(PatchError {
message: format!(
"index {at} is out of bounds for array of length {len}",
at = index.unwrap_or(len),
),
})
};
match mode {
Write::Insert => {
let at = index.unwrap_or(len);
if at > len {
bail!(out_of_bounds());
}
v.insert(at, value);
}
Write::Overwrite => {
let Some(slot) = index.and_then(|at| v.get_mut(at)) else {
bail!(out_of_bounds());
};
*slot = value;
}
}
return Ok(());
}
Located::Many => {
bail!(Error::InvalidPatch(PatchError {
message: format!(
"'{last}' names a position, but the path before it reaches every element of a sequence rather than one of them",
),
}))
}
Located::Found(_) | Located::Absent => (),
}
}
this.put(&Operation::path_to_parts(path), value);
Ok(())
}
impl Value {
pub fn patch(&mut self, ops: Value) -> Result<()> {
let mut this = self.clone();
for operation in Operation::value_to_operations(ops)
.map_err(Error::InvalidPatch)
.map_err(anyhow::Error::new)?
{
match operation {
Operation::Add {
path,
value,
} => {
let names_position = path.last().is_some_and(|last| {
last.as_str() == "-" || Operation::array_index(last.as_str()).is_some()
});
if !names_position
&& matches!(locate(&mut this, &path), Located::Found(Value::Array(_)))
{
this.inc(&Operation::path_to_parts(&path), value)?;
} else {
write(&mut this, &path, value, Write::Insert)?;
}
}
Operation::Remove {
path,
} => {
let path = Operation::path_to_parts(&path);
this.cut(&path);
}
Operation::Replace {
path,
value,
} => write(&mut this, &path, value, Write::Overwrite)?,
Operation::Change {
path,
value,
} => {
let parts = Operation::path_to_parts(&path);
if let Value::String(p) = value
&& let Value::String(v) = this.pick(&parts)
{
let dmp = dmp::new();
let pch = dmp.patch_from_text(p.into_string()).map_err(|e| {
Error::InvalidPatch(PatchError {
message: format!("{e:?}"),
})
})?;
let (txt, _) = dmp.patch_apply(&pch, v.as_str()).map_err(|e| {
Error::InvalidPatch(PatchError {
message: format!("{e:?}"),
})
})?;
let txt = txt.into_iter().collect::<String>();
write(&mut this, &path, Value::from(txt), Write::Overwrite)?;
}
}
Operation::Copy {
path,
from,
} => {
let val = this.pick(&Operation::path_to_parts(&from));
write(&mut this, &path, val, Write::Insert)?;
}
Operation::Move {
path,
from,
} => {
let from = Operation::path_to_parts(&from);
let val = this.pick(&from);
this.cut(&from);
write(&mut this, &path, val, Write::Insert)?;
}
Operation::Test {
path,
value,
} => {
let path = Operation::path_to_parts(&path);
let val = this.pick(&path);
ensure!(
value == val,
Error::PatchTest {
expected: value.to_sql(),
got: val.to_sql(),
}
);
}
}
}
normalize_sets(&mut this);
*self = this;
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::expr::Operation;
use crate::syn;
macro_rules! parse_val {
($input:expr) => {
crate::val::convert_public_value_to_internal(syn::value($input).unwrap())
};
}
#[tokio::test]
async fn patch_add_simple() {
let mut val = parse_val!("{ test: { other: null, something: 123 } }");
let ops = parse_val!("[{ op: 'add', path: '/temp', value: true }]");
let res = parse_val!("{ test: { other: null, something: 123 }, temp: true }");
val.patch(ops).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_remove_simple() {
let mut val = parse_val!("{ test: { other: null, something: 123 }, temp: true }");
let ops = parse_val!("[{ op: 'remove', path: '/temp' }]");
let res = parse_val!("{ test: { other: null, something: 123 } }");
val.patch(ops).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_replace_simple() {
let mut val = parse_val!("{ test: { other: null, something: 123 }, temp: true }");
let ops = parse_val!("[{ op: 'replace', path: '/temp', value: 'text' }]");
let res = parse_val!("{ test: { other: null, something: 123 }, temp: 'text' }");
val.patch(ops).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_change_simple() {
let mut val = parse_val!("{ test: { other: null, something: 123 }, temp: 'test' }");
let ops = parse_val!(
"[{ op: 'change', path: '/temp', value: '@@ -1,4 +1,4 @@\n te\n-s\n+x\n t\n' }]"
);
let res = parse_val!("{ test: { other: null, something: 123 }, temp: 'text' }");
val.patch(ops).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_copy_simple() {
let mut val = parse_val!("{ test: 123, temp: true }");
let ops = parse_val!("[{ op: 'copy', path: '/temp', from: '/test' }]");
let res = parse_val!("{ test: 123, temp: 123 }");
val.patch(ops).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_move_simple() {
let mut val = parse_val!("{ temp: true, some: 123 }");
let ops = parse_val!("[{ op: 'move', path: '/other', from: '/temp' }]");
let res = parse_val!("{ other: true, some: 123 }");
val.patch(ops).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_test_simple() {
let mut val = parse_val!("{ test: { other: 'test', something: 123 }, temp: true }");
let ops = parse_val!(
"[{ op: 'remove', path: '/test/something' }, { op: 'test', path: '/temp', value: true }]"
);
let res = parse_val!("{ test: { other: 'test' }, temp: true }");
val.patch(ops).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_add_embedded() {
let mut val = parse_val!("{ test: { other: null, something: 123 } }");
let ops = parse_val!("[{ op: 'add', path: '/temp/test', value: true }]");
let res = parse_val!("{ test: { other: null, something: 123 }, temp: { test: true } }");
val.patch(ops).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_remove_embedded() {
let mut val = parse_val!("{ test: { other: null, something: 123 }, temp: true }");
let ops = parse_val!("[{ op: 'remove', path: '/test/other' }]");
let res = parse_val!("{ test: { something: 123 }, temp: true }");
val.patch(ops).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_remove_array_index() {
let mut val = parse_val!("{ id: todo:1 }");
let add = parse_val!("[{ op: 'add', path: '/list', value: ['Item here'] }]");
let remove = parse_val!("[{ op: 'remove', path: '/list/0' }]");
let res = parse_val!("{ id: todo:1, list: [] }");
val.patch(add).unwrap();
val.patch(remove).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_add_array_index_append_at_length() {
let mut val = parse_val!("{ list: ['a', 'b'] }");
let ops = parse_val!("[{ op: 'add', path: '/list/2', value: 'c' }]");
let res = parse_val!("{ list: ['a', 'b', 'c'] }");
val.patch(ops).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_add_array_index_out_of_bounds_errors() {
let mut val = parse_val!("{ list: ['a', 'b'] }");
let ops = parse_val!("[{ op: 'add', path: '/list/5', value: 'c' }]");
let err = val.patch(ops).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("index 5 is out of bounds for array of length 2"),
"unexpected error message: {msg}"
);
assert_eq!(val, parse_val!("{ list: ['a', 'b'] }"));
}
#[tokio::test]
async fn patch_replace_embedded() {
let mut val = parse_val!("{ test: { other: null, something: 123 }, temp: true }");
let ops = parse_val!("[{ op: 'replace', path: '/test/other', value: 'text' }]");
let res = parse_val!("{ test: { other: 'text', something: 123 }, temp: true }");
val.patch(ops).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_change_embedded() {
let mut val = parse_val!("{ test: { other: 'test', something: 123 }, temp: true }");
let ops = parse_val!(
"[{ op: 'change', path: '/test/other', value: '@@ -1,4 +1,4 @@\n te\n-s\n+x\n t\n' }]"
);
let res = parse_val!("{ test: { other: 'text', something: 123 }, temp: true }");
val.patch(ops).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_copy_embedded() {
let mut val = parse_val!("{ test: { other: null }, temp: 123 }");
let ops = parse_val!("[{ op: 'copy', path: '/test/other', from: '/temp' }]");
let res = parse_val!("{ test: { other: 123 }, temp: 123 }");
val.patch(ops).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_move_embedded() {
let mut val = parse_val!("{ test: { other: ':3', some: 123 }}");
let ops = parse_val!("[{ op: 'move', path: '/temp', from: '/test/other' }]");
let res = parse_val!("{ test: { some: 123 }, temp: ':3' }");
val.patch(ops).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_test_embedded() {
let mut val = parse_val!("{ test: { other: 'test', something: 123 }, temp: true }");
let ops = parse_val!(
"[{ op: 'remove', path: '/test/other' }, { op: 'test', path: '/test/something', value: 123 }]"
);
let res = parse_val!("{ test: { something: 123 }, temp: true }");
val.patch(ops).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_change_invalid() {
let mut val = parse_val!("{ test: { other: 'test', something: 123 }, temp: true }");
let ops = parse_val!("[{ op: 'change', path: '/test/other', value: 'text' }]");
assert!(val.patch(ops).is_err());
}
#[tokio::test]
async fn patch_test_invalid() {
let mut val = parse_val!("{ test: { other: 'test', something: 123 }, temp: true }");
let should = val.clone();
let ops = parse_val!(
"[{ op: 'remove', path: '/test/other' }, { op: 'test', path: '/test/something', value: 'not same' }]"
);
assert!(val.patch(ops).is_err());
assert_eq!(val, should);
}
#[tokio::test]
async fn patch_change_root() {
let mut val = parse_val!("'Hello'");
let ops = parse_val!(
"[{ op: 'change', path: '', value: '@@ -1,5 +1,12 @@\n Hello\n+ there!\n' }]"
);
val.patch(ops).unwrap();
assert_eq!(val, parse_val!("'Hello there!'"));
}
#[tokio::test]
async fn patch_replace_root() {
let mut val = parse_val!("1");
let ops = parse_val!("[{ op: 'replace', path: '', value: 2 }]");
val.patch(ops).unwrap();
assert_eq!(val, parse_val!("2"));
}
#[tokio::test]
async fn patch_diff_roundtrip_string_root() {
let mut start = parse_val!("'Hello'");
let after = parse_val!("'Hello there!'");
let ops = Operation::operations_to_value(start.diff(&after));
start.patch(ops).unwrap();
assert_eq!(start, after);
}
#[tokio::test]
async fn patch_replace_array_element_leaf() {
let mut val = parse_val!("{ details: [{ isbn: 'a' }, { isbn: 'b' }] }");
let ops = parse_val!("[{ op: 'replace', path: '/details/0/isbn', value: 'z' }]");
let res = parse_val!("{ details: [{ isbn: 'z' }, { isbn: 'b' }] }");
val.patch(ops).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_replace_whole_array_element() {
let mut val = parse_val!("{ details: [{ isbn: 'a' }, { isbn: 'b' }] }");
let ops = parse_val!("[{ op: 'replace', path: '/details/1', value: 'gone' }]");
let res = parse_val!("{ details: [{ isbn: 'a' }, 'gone'] }");
val.patch(ops).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_replace_nested_array_index() {
let mut val = parse_val!("{ a: [[1, 2], [3, 4]] }");
let ops = parse_val!("[{ op: 'replace', path: '/a/0/1', value: 99 }]");
let res = parse_val!("{ a: [[1, 99], [3, 4]] }");
val.patch(ops).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_change_array_element() {
let mut val = parse_val!("{ tags: ['test', 'keep'] }");
let ops = parse_val!(
"[{ op: 'change', path: '/tags/0', value: '@@ -1,4 +1,4 @@\n te\n-s\n+x\n t\n' }]"
);
let res = parse_val!("{ tags: ['text', 'keep'] }");
val.patch(ops).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_copy_from_array_element() {
let mut val = parse_val!("{ list: ['p', 'q'] }");
let ops = parse_val!("[{ op: 'copy', path: '/first', from: '/list/0' }]");
let res = parse_val!("{ list: ['p', 'q'], first: 'p' }");
val.patch(ops).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_move_from_array_element() {
let mut val = parse_val!("{ list: ['p', 'q'] }");
let ops = parse_val!("[{ op: 'move', path: '/moved', from: '/list/0' }]");
let res = parse_val!("{ list: ['q'], moved: 'p' }");
val.patch(ops).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_test_array_element() {
let mut val = parse_val!("{ rows: [{ n: 1 }, { n: 2 }] }");
let ops = parse_val!("[{ op: 'test', path: '/rows/1/n', value: 2 }]");
val.patch(ops).unwrap();
assert_eq!(val, parse_val!("{ rows: [{ n: 1 }, { n: 2 }] }"));
let mut val = parse_val!("{ rows: [{ n: 1 }, { n: 2 }] }");
let ops = parse_val!("[{ op: 'test', path: '/rows/1/n', value: 1 }]");
assert!(val.patch(ops).is_err());
}
#[tokio::test]
async fn patch_add_intermediate_array_index() {
let mut val = parse_val!("{ details: [{ isbn: 'a' }, { isbn: 'b' }] }");
let ops = parse_val!("[{ op: 'add', path: '/details/0/title', value: 'T' }]");
let res = parse_val!("{ details: [{ isbn: 'a', title: 'T' }, { isbn: 'b' }] }");
val.patch(ops).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_add_array_index_under_array_index() {
let mut val = parse_val!("{ a: [['x']] }");
let ops = parse_val!("[{ op: 'add', path: '/a/0/1', value: 'y' }]");
val.patch(ops).unwrap();
assert_eq!(val, parse_val!("{ a: [['x', 'y']] }"));
let mut val = parse_val!("{ a: [['x']] }");
let ops = parse_val!("[{ op: 'add', path: '/a/0/5', value: 'y' }]");
let err = val.patch(ops).unwrap_err();
assert!(
err.to_string().contains("index 5 is out of bounds for array of length 1"),
"unexpected error message: {err}"
);
assert_eq!(val, parse_val!("{ a: [['x']] }"));
}
#[tokio::test]
async fn patch_object_member_named_as_an_index() {
let mut val = parse_val!("{ m: { '0': 'zero', '1': 'one' } }");
let ops = parse_val!(
"[{ op: 'replace', path: '/m/0', value: 'Z' }, { op: 'remove', path: '/m/1' }]"
);
let res = parse_val!("{ m: { '0': 'Z' } }");
val.patch(ops).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_object_member_not_spelled_as_an_index() {
let mut val =
parse_val!("{ m: { '007': 'bond', '7': 'seven', '+3': 'plus', '-1': 'neg' } }");
let ops = parse_val!(
"[{ op: 'remove', path: '/m/007' }, { op: 'remove', path: '/m/+3' }, { op: 'replace', path: '/m/-1', value: 'N' }]"
);
let res = parse_val!("{ m: { '7': 'seven', '-1': 'N' } }");
val.patch(ops).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_copy_into_array_position_inserts() {
let mut val = parse_val!("{ list: ['a', 'c'], v: 'b' }");
let ops = parse_val!("[{ op: 'copy', path: '/list/1', from: '/v' }]");
let res = parse_val!("{ list: ['a', 'b', 'c'], v: 'b' }");
val.patch(ops).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_move_within_one_array() {
let mut val = parse_val!("{ list: ['a', 'b', 'c'] }");
let ops = parse_val!("[{ op: 'move', path: '/list/2', from: '/list/0' }]");
let res = parse_val!("{ list: ['b', 'c', 'a'] }");
val.patch(ops).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_move_into_array_position_inserts() {
let mut val = parse_val!("{ list: ['a', 'c'], v: 'b' }");
let ops = parse_val!("[{ op: 'move', path: '/list/1', from: '/v' }]");
let res = parse_val!("{ list: ['a', 'b', 'c'] }");
val.patch(ops).unwrap();
assert_eq!(res, val);
}
#[tokio::test]
async fn patch_append_token_appends_or_names_a_member() {
let mut val = parse_val!("{ list: ['a'], v: 'b' }");
let ops = parse_val!(
"[{ op: 'add', path: '/list/-', value: 'x' }, { op: 'copy', path: '/list/-', from: '/v' }]"
);
val.patch(ops).unwrap();
assert_eq!(val, parse_val!("{ list: ['a', 'x', 'b'], v: 'b' }"));
let mut val = parse_val!("{ m: { a: 1 } }");
let ops = parse_val!("[{ op: 'add', path: '/m/-', value: 2 }]");
val.patch(ops).unwrap();
assert_eq!(val, parse_val!("{ m: { a: 1, '-': 2 } }"));
}
#[tokio::test]
async fn patch_overwrite_needs_a_position_that_exists() {
for pointer in ["/list/9", "/list/2", "/list/-"] {
let mut val = parse_val!("{ list: ['a', 'b'] }");
let ops = parse_val!(&format!("[{{ op: 'replace', path: '{pointer}', value: 'z' }}]"));
let err = val.patch(ops).unwrap_err();
assert!(
err.to_string().contains("is out of bounds for array of length 2"),
"unexpected error for {pointer}: {err}"
);
assert_eq!(val, parse_val!("{ list: ['a', 'b'] }"));
}
}
#[tokio::test]
async fn patch_add_index_on_a_non_array_names_a_member() {
let mut val = parse_val!("{ m: { x: 1 } }");
let ops = parse_val!("[{ op: 'add', path: '/m/0', value: 5 }]");
val.patch(ops).unwrap();
assert_eq!(val, parse_val!("{ m: { x: 1, '0': 5 } }"));
}
#[tokio::test]
async fn patch_set_keeps_its_invariant() {
let mut val = parse_val!("{ s: { 'b', 'c', } }");
val.patch(parse_val!("[{ op: 'replace', path: '/s/0', value: 'z' }]")).unwrap();
assert_eq!(val, parse_val!("{ s: { 'c', 'z', } }"));
let mut val = parse_val!("{ s: { 'b', 'c', } }");
val.patch(parse_val!("[{ op: 'replace', path: '/s/0', value: 'c' }]")).unwrap();
assert_eq!(val, parse_val!("{ s: { 'c', } }"));
let mut val = parse_val!("{ s: { { n: 'zz' }, { n: 'aa' }, } }");
val.patch(parse_val!("[{ op: 'replace', path: '/s/0/n', value: 'zz' }]")).unwrap();
assert_eq!(val, parse_val!("{ s: { { n: 'zz' }, } }"));
let mut val = parse_val!("{ s: { [{ inner: { 'q', 'p', } }], } }");
val.patch(parse_val!("[{ op: 'replace', path: '/s/0/0/inner', value: { 'y', 'x', } }]"))
.unwrap();
assert_eq!(val, parse_val!("{ s: { [{ inner: { 'x', 'y', } }], } }"));
}
#[tokio::test]
async fn patch_position_under_a_broadcast_path_is_refused() {
for op in [
"{ op: 'replace', path: '/list/name/0', value: 'X' }",
"{ op: 'add', path: '/list/name/0', value: 'X' }",
"{ op: 'copy', path: '/list/name/0', from: '/v' }",
] {
let mut val = parse_val!("{ list: [{ name: 'AA' }, { name: 'BB' }], v: 'X' }");
let err = val.patch(parse_val!(&format!("[{op}]"))).unwrap_err();
assert!(
err.to_string().contains("reaches every element of a sequence"),
"unexpected error for {op}: {err}"
);
assert_eq!(val, parse_val!("{ list: [{ name: 'AA' }, { name: 'BB' }], v: 'X' }"));
}
}
#[tokio::test]
async fn patch_move_keeps_its_source_when_the_destination_is_refused() {
let mut val = parse_val!("{ list: [{ name: 'AA' }], v: 'keep' }");
let ops = parse_val!("[{ op: 'move', path: '/list/name/0', from: '/v' }]");
assert!(val.patch(ops).is_err());
assert_eq!(val, parse_val!("{ list: [{ name: 'AA' }], v: 'keep' }"));
}
#[tokio::test]
async fn patch_destination_inside_the_source_is_refused() {
for op in [
"{ op: 'copy', path: '/a/-', from: '/a' }",
"{ op: 'copy', path: '/a/0/b', from: '/a' }",
"{ op: 'move', path: '/a/b', from: '/a' }",
] {
let mut val = parse_val!("{ a: [1, 2] }");
let err = val.patch(parse_val!(&format!("[{op}]"))).unwrap_err();
assert!(
err.to_string().contains("that is not inside 'from'"),
"unexpected error for {op}: {err}"
);
assert_eq!(val, parse_val!("{ a: [1, 2] }"));
}
let mut val = parse_val!("{ a: [1, 2], ab: 'x' }");
val.patch(parse_val!("[{ op: 'copy', path: '/ab', from: '/a' }]")).unwrap();
assert_eq!(val, parse_val!("{ a: [1, 2], ab: [1, 2] }"));
}
#[tokio::test]
async fn patch_diff_roundtrip_array_element() {
for (a, b) in [
("{ list: [1, 2, 3] }", "{ list: [1, 9, 3] }"),
("{ list: ['aa', 'bb'] }", "{ list: ['aa', 'bx'] }"),
("{ list: [{ n: 1 }, { n: 2 }] }", "{ list: [{ n: 1 }, { n: 7 }] }"),
("{ list: [1] }", "{ list: [1, 2, 3] }"),
("{ list: [1, 2, 3] }", "{ list: [1] }"),
("{ list: [1, 2, 3, 4] }", "{ list: [9] }"),
("{ list: [{ n: 1 }, { n: 2 }, { n: 3 }] }", "{ list: [{ n: 5 }] }"),
] {
let mut start = parse_val!(a);
let after = parse_val!(b);
let ops = Operation::operations_to_value(start.diff(&after));
start.patch(ops).unwrap();
assert_eq!(start, after, "diff of {a} -> {b} did not round-trip");
}
}
#[tokio::test]
async fn patch_diff_roundtrip_number_root() {
let mut start = parse_val!("1");
let after = parse_val!("2");
let ops = Operation::operations_to_value(start.diff(&after));
start.patch(ops).unwrap();
assert_eq!(start, after);
}
}