use serde::{Deserialize, Serialize};
use serde_json::Value;
pub type JsonPatch = Vec<JsonPatchOperation>;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "op", rename_all = "lowercase")]
pub enum JsonPatchOperation {
Add { path: String, value: Value },
Remove { path: String },
Replace { path: String, value: Value },
Move { from: String, path: String },
Copy { from: String, path: String },
Test { path: String, value: Value },
}
pub fn diff(old: &Value, new: &Value) -> JsonPatch {
let mut patch = JsonPatch::new();
diff_at(String::new(), old, new, &mut patch);
patch
}
fn diff_at(path: String, old: &Value, new: &Value, patch: &mut JsonPatch) {
if old == new {
return;
}
match (old, new) {
(Value::Object(old), Value::Object(new)) => {
for key in old.keys().filter(|key| !new.contains_key(*key)) {
patch.push(JsonPatchOperation::Remove {
path: child(&path, key),
});
}
for (key, value) in new {
match old.get(key) {
Some(previous) => diff_at(child(&path, key), previous, value, patch),
None => patch.push(JsonPatchOperation::Add {
path: child(&path, key),
value: value.clone(),
}),
}
}
}
(Value::Array(old), Value::Array(new)) => {
let common = old.len().min(new.len());
for (index, (previous, value)) in old.iter().zip(new).enumerate() {
diff_at(child(&path, &index.to_string()), previous, value, patch);
}
for (index, value) in new.iter().enumerate().skip(common) {
patch.push(JsonPatchOperation::Add {
path: child(&path, &index.to_string()),
value: value.clone(),
});
}
for index in (common..old.len()).rev() {
patch.push(JsonPatchOperation::Remove {
path: child(&path, &index.to_string()),
});
}
}
_ => patch.push(JsonPatchOperation::Replace {
path,
value: new.clone(),
}),
}
}
fn child(path: &str, token: &str) -> String {
format!("{path}/{}", token.replace('~', "~0").replace('/', "~1"))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn apply(mut doc: Value, patch: &JsonPatch) -> Value {
for op in patch {
let (path, value) = match op {
JsonPatchOperation::Add { path, value }
| JsonPatchOperation::Replace { path, value } => (path, Some(value.clone())),
JsonPatchOperation::Remove { path } => (path, None),
other => panic!("diff emitted {other:?}"),
};
if path.is_empty() {
doc = value.expect("root remove");
continue;
}
let (parent, last) = path.rsplit_once('/').unwrap();
let last = last.replace("~1", "/").replace("~0", "~");
match doc.pointer_mut(parent).expect("parent exists") {
Value::Object(map) => match op {
JsonPatchOperation::Remove { .. } => {
map.remove(&last).expect("removed key exists");
}
JsonPatchOperation::Replace { .. } => {
assert!(map.contains_key(&last), "replace of a missing key");
map.insert(last, value.unwrap());
}
_ => {
map.insert(last, value.unwrap());
}
},
Value::Array(items) => {
let index: usize = last.parse().unwrap();
match op {
JsonPatchOperation::Add { .. } => {
assert!(index <= items.len(), "add past the end");
items.insert(index, value.unwrap());
}
JsonPatchOperation::Remove { .. } => {
assert!(index < items.len(), "remove past the end");
items.remove(index);
}
_ => {
assert!(index < items.len(), "replace past the end");
items[index] = value.unwrap();
}
}
}
other => panic!("cannot apply {op:?} under {other}"),
}
}
doc
}
fn round_trip(old: Value, new: Value) -> JsonPatch {
let patch = diff(&old, &new);
assert_eq!(apply(old, &patch), new, "patch {patch:?}");
patch
}
#[test]
fn equal_values_need_no_ops() {
let value = json!({ "a": [1, { "b": null }] });
assert!(diff(&value, &value.clone()).is_empty());
}
#[test]
fn object_keys_are_added_removed_and_replaced() {
let patch = round_trip(
json!({ "keep": 1, "gone": true, "change": "x" }),
json!({ "keep": 1, "change": "y", "new": [1] }),
);
assert_eq!(patch.len(), 3);
assert!(patch.contains(&JsonPatchOperation::Remove {
path: "/gone".into()
}));
assert!(patch.contains(&JsonPatchOperation::Replace {
path: "/change".into(),
value: json!("y"),
}));
assert!(patch.contains(&JsonPatchOperation::Add {
path: "/new".into(),
value: json!([1]),
}));
}
#[test]
fn arrays_grow_and_shrink_from_the_end() {
let grown = round_trip(json!([1, 2]), json!([1, 5, 3, 4]));
assert_eq!(grown.len(), 3);
let shrunk = round_trip(json!([1, 2, 3, 4]), json!([9]));
assert_eq!(
shrunk,
vec![
JsonPatchOperation::Replace {
path: "/0".into(),
value: json!(9)
},
JsonPatchOperation::Remove { path: "/3".into() },
JsonPatchOperation::Remove { path: "/2".into() },
JsonPatchOperation::Remove { path: "/1".into() },
]
);
round_trip(json!([1]), json!([]));
round_trip(json!([]), json!([{ "a": 1 }]));
}
#[test]
fn nested_changes_patch_the_leaf() {
let patch = round_trip(
json!({ "todos": [{ "content": "a", "status": "pending" }] }),
json!({ "todos": [{ "content": "a", "status": "completed" }] }),
);
assert_eq!(
patch,
vec![JsonPatchOperation::Replace {
path: "/todos/0/status".into(),
value: json!("completed"),
}]
);
}
#[test]
fn type_changes_and_the_root_are_replaced() {
round_trip(json!({ "a": [1] }), json!({ "a": { "0": 1 } }));
round_trip(json!([1]), json!({ "a": 1 }));
let patch = round_trip(json!(1), json!("one"));
assert_eq!(
patch,
vec![JsonPatchOperation::Replace {
path: String::new(),
value: json!("one")
}]
);
}
#[test]
fn keys_are_escaped_as_json_pointer_tokens() {
let patch = round_trip(json!({}), json!({ "a/b": 1, "c~d": 2 }));
let mut paths: Vec<_> = patch
.iter()
.map(|op| match op {
JsonPatchOperation::Add { path, .. } => path.as_str(),
other => panic!("{other:?}"),
})
.collect();
paths.sort_unstable();
assert_eq!(paths, ["/a~1b", "/c~0d"]);
round_trip(
json!({ "a/b": { "c~d": 1 } }),
json!({ "a/b": { "c~d": 2 } }),
);
}
}