use crate::sql::idiom::Idiom; use crate::sql::operation::{Op, Operation}; use crate::sql::value::Value; use std::cmp::min; impl Value { pub fn diff(&self, val: &Value, path: Idiom) -> Vec { let mut ops: Vec = vec![]; match (self, val) { (Value::Object(a), Value::Object(b)) if a != b => { // Loop over old keys for (key, _) in a.value.iter() { if b.value.contains_key(key) == false { ops.push(Operation { op: Op::Remove, path: path.add(key.clone().into()), value: Value::Null, }) } } // Loop over new keys for (key, val) in b.value.iter() { match a.value.get(key) { None => ops.push(Operation { op: Op::Add, path: path.add(key.clone().into()), value: val.clone(), }), Some(old) => { let path = path.add(key.clone().into()); ops.append(&mut old.diff(val, path)) } } } } (Value::Array(a), Value::Array(b)) if a != b => { let mut n = 0; while n < min(a.len(), b.len()) { let path = path.add(n.into()); ops.append(&mut a.value[n].diff(&b.value[n], path)); n += 1; } while n < b.len() { if n >= a.len() { ops.push(Operation { op: Op::Add, path: path.add(n.into()), value: b.value[n].clone(), }) } n += 1; } while n < a.len() { if n >= b.len() { ops.push(Operation { op: Op::Remove, path: path.add(n.into()), value: Value::Null, }) } n += 1; } } (Value::Strand(a), Value::Strand(b)) if a != b => ops.push(Operation { op: Op::Change, path, value: { let mut dmp = dmp::new(); let mut pch = dmp.patch_make1(&a.value, &b.value); let txt = dmp.patch_to_text(&mut pch); txt.into() }, }), (a, b) if a != b => ops.push(Operation { op: Op::Replace, path, value: val.clone(), }), (_, _) => (), } ops } } #[cfg(test)] mod tests { use super::*; use crate::sql::array::Array; use crate::sql::test::Parse; #[test] fn diff_none() { let old = Value::parse("{ test: true, text: 'text', other: { something: true } }"); let now = Value::parse("{ test: true, text: 'text', other: { something: true } }"); let res = Array::parse("[]"); assert_eq!(res.to_operations().unwrap(), old.diff(&now, Idiom::default())); } #[test] fn diff_add() { let old = Value::parse("{ test: true }"); let now = Value::parse("{ test: true, other: 'test' }"); let res = Array::parse("[{ op: 'add', path: '/other', value: 'test' }]"); assert_eq!(res.to_operations().unwrap(), old.diff(&now, Idiom::default())); } #[test] fn diff_remove() { let old = Value::parse("{ test: true, other: 'test' }"); let now = Value::parse("{ test: true }"); let res = Array::parse("[{ op: 'remove', path: '/other' }]"); assert_eq!(res.to_operations().unwrap(), old.diff(&now, Idiom::default())); } #[test] fn diff_add_array() { let old = Value::parse("{ test: [1,2,3] }"); let now = Value::parse("{ test: [1,2,3,4] }"); let res = Array::parse("[{ op: 'add', path: '/test/3', value: 4 }]"); assert_eq!(res.to_operations().unwrap(), old.diff(&now, Idiom::default())); } #[test] fn diff_replace_embedded() { let old = Value::parse("{ test: { other: 'test' } }"); let now = Value::parse("{ test: { other: false } }"); let res = Array::parse("[{ op: 'replace', path: '/test/other', value: false }]"); assert_eq!(res.to_operations().unwrap(), old.diff(&now, Idiom::default())); } #[test] fn diff_change_text() { let old = Value::parse("{ test: { other: 'test' } }"); let now = Value::parse("{ test: { other: 'text' } }"); let res = Array::parse( "[{ op: 'change', path: '/test/other', value: '@@ -1,4 +1,4 @@\n te\n-s\n+x\n t\n' }]", ); assert_eq!(res.to_operations().unwrap(), old.diff(&now, Idiom::default())); } }