use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "op", rename_all = "lowercase")]
pub enum PatchOperation {
Add {
path: String,
value: Value,
},
Remove {
path: String,
},
Replace {
path: String,
value: Value,
},
Copy {
from: String,
path: String,
},
Move {
from: String,
path: String,
},
Test {
path: String,
value: Value,
},
}
pub type JsonPatch = Vec<PatchOperation>;
pub fn compute_diff(old: &Value, new: &Value) -> JsonPatch {
let mut patch = Vec::new();
compute_diff_recursive(old, new, "", &mut patch);
patch
}
fn compute_diff_recursive(old: &Value, new: &Value, path: &str, patch: &mut JsonPatch) {
match (old, new) {
(Value::Object(old_map), Value::Object(new_map)) => {
for (key, _old_val) in old_map {
if !new_map.contains_key(key) {
let full_path = if path.is_empty() {
format!("/{}", escape_json_pointer(key))
} else {
format!("{}/{}", path, escape_json_pointer(key))
};
patch.push(PatchOperation::Remove { path: full_path });
}
}
for (key, new_val) in new_map {
let full_path = if path.is_empty() {
format!("/{}", escape_json_pointer(key))
} else {
format!("{}/{}", path, escape_json_pointer(key))
};
match old_map.get(key) {
Some(old_val) => {
if old_val != new_val {
if (old_val.is_object() && new_val.is_object())
|| (old_val.is_array() && new_val.is_array())
{
compute_diff_recursive(old_val, new_val, &full_path, patch);
} else {
patch.push(PatchOperation::Replace {
path: full_path,
value: new_val.clone(),
});
}
}
}
None => {
patch.push(PatchOperation::Add {
path: full_path,
value: new_val.clone(),
});
}
}
}
}
(Value::Array(old_arr), Value::Array(new_arr)) => {
if old_arr == new_arr {
return;
}
let max_len = old_arr.len().max(new_arr.len());
let size_diff = (old_arr.len() as i64 - new_arr.len() as i64).abs();
let threshold = (max_len / 2).max(5) as i64;
if size_diff > threshold {
patch.push(PatchOperation::Replace {
path: path.to_string(),
value: new.clone(),
});
return;
}
let mut removes: Vec<usize> = Vec::new();
for i in 0..max_len {
let elem_path = format!("{}/{}", path, i);
match (old_arr.get(i), new_arr.get(i)) {
(Some(old_e), Some(new_e)) if old_e != new_e => {
compute_diff_recursive(old_e, new_e, &elem_path, patch);
}
(Some(_), None) => {
removes.push(i);
}
(None, Some(new_e)) => {
patch.push(PatchOperation::Add {
path: format!("{}/-", path),
value: new_e.clone(),
});
}
_ => {
}
}
}
for i in removes.into_iter().rev() {
patch.push(PatchOperation::Remove {
path: format!("{}/{}", path, i),
});
}
}
_ => {
if old != new {
patch.push(PatchOperation::Replace {
path: path.to_string(),
value: new.clone(),
});
}
}
}
}
pub fn apply_patch(doc: &mut Value, patch: &JsonPatch) -> Result<(), PatchError> {
for op in patch {
apply_operation(doc, op)?;
}
Ok(())
}
fn apply_operation(doc: &mut Value, op: &PatchOperation) -> Result<(), PatchError> {
match op {
PatchOperation::Add { path, value } => {
let pointer = json_pointer::JsonPointer::new(path);
pointer.add(doc, value.clone())?;
}
PatchOperation::Remove { path } => {
let pointer = json_pointer::JsonPointer::new(path);
pointer.remove(doc)?;
}
PatchOperation::Replace { path, value } => {
let pointer = json_pointer::JsonPointer::new(path);
pointer.replace(doc, value.clone())?;
}
PatchOperation::Copy { from, path } => {
let from_ptr = json_pointer::JsonPointer::new(from);
let value = from_ptr
.get(doc)?
.cloned()
.ok_or(PatchError::PathNotFound)?;
let to_ptr = json_pointer::JsonPointer::new(path);
to_ptr.add(doc, value)?;
}
PatchOperation::Move { from, path } => {
let from_ptr = json_pointer::JsonPointer::new(from);
let value = from_ptr.remove(doc)?;
let to_ptr = json_pointer::JsonPointer::new(path);
to_ptr.add(doc, value)?;
}
PatchOperation::Test { path, value } => {
let pointer = json_pointer::JsonPointer::new(path);
let actual = pointer.get(doc)?.ok_or(PatchError::PathNotFound)?;
if actual != value {
return Err(PatchError::TestFailed);
}
}
}
Ok(())
}
fn escape_json_pointer(s: &str) -> String {
s.replace('~', "~0").replace('/', "~1")
}
fn _unescape_json_pointer(s: &str) -> String {
s.replace("~1", "/").replace("~0", "~")
}
#[derive(Debug, Clone, PartialEq)]
pub enum PatchError {
PathNotFound,
InvalidPath,
TestFailed,
CannotReplaceRoot,
}
impl std::fmt::Display for PatchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PatchError::PathNotFound => write!(f, "Path not found"),
PatchError::InvalidPath => write!(f, "Invalid JSON Pointer path"),
PatchError::TestFailed => write!(f, "Test operation failed"),
PatchError::CannotReplaceRoot => write!(f, "Cannot replace root document"),
}
}
}
impl std::error::Error for PatchError {}
mod json_pointer {
use super::{PatchError, Value};
pub struct JsonPointer {
tokens: Vec<String>,
}
impl JsonPointer {
pub fn new(path: &str) -> Self {
let tokens: Vec<String> = if path.is_empty() || path == "/" {
vec![]
} else {
path.split('/')
.skip(1) .map(|s| s.replace("~1", "/").replace("~0", "~"))
.collect()
};
Self { tokens }
}
pub fn get<'a>(&self, doc: &'a Value) -> Result<Option<&'a Value>, PatchError> {
let mut current = doc;
for token in &self.tokens {
current = match current {
Value::Object(map) => map.get(token),
Value::Array(arr) => {
if token == "-" {
arr.last()
} else {
token.parse::<usize>().ok().and_then(|i| arr.get(i))
}
}
_ => return Err(PatchError::InvalidPath),
}
.ok_or(PatchError::PathNotFound)?;
}
Ok(Some(current))
}
pub fn add(&self, doc: &mut Value, value: Value) -> Result<(), PatchError> {
if self.tokens.is_empty() {
return Err(PatchError::CannotReplaceRoot);
}
let mut current = doc;
let last = self.tokens.len() - 1;
for (i, token) in self.tokens.iter().enumerate() {
if i == last {
match current {
Value::Object(map) => {
map.insert(token.clone(), value);
}
Value::Array(arr) => {
if token == "-" {
arr.push(value);
} else {
let idx = token
.parse::<usize>()
.map_err(|_| PatchError::InvalidPath)?;
if idx > arr.len() {
return Err(PatchError::PathNotFound);
}
arr.insert(idx, value);
}
}
_ => return Err(PatchError::InvalidPath),
}
return Ok(());
}
current = match current {
Value::Object(map) => map.get_mut(token).ok_or(PatchError::PathNotFound)?,
Value::Array(arr) => {
let idx = token
.parse::<usize>()
.map_err(|_| PatchError::InvalidPath)?;
arr.get_mut(idx).ok_or(PatchError::PathNotFound)?
}
_ => return Err(PatchError::InvalidPath),
};
}
Ok(())
}
pub fn remove(&self, doc: &mut Value) -> Result<Value, PatchError> {
if self.tokens.is_empty() {
return Err(PatchError::CannotReplaceRoot);
}
let mut current = doc;
let last = self.tokens.len() - 1;
for (i, token) in self.tokens.iter().enumerate() {
if i == last {
return match current {
Value::Object(map) => map.remove(token).ok_or(PatchError::PathNotFound),
Value::Array(arr) => {
let idx = token
.parse::<usize>()
.map_err(|_| PatchError::InvalidPath)?;
if idx >= arr.len() {
return Err(PatchError::PathNotFound);
}
Ok(arr.remove(idx))
}
_ => Err(PatchError::InvalidPath),
};
}
current = match current {
Value::Object(map) => map.get_mut(token).ok_or(PatchError::PathNotFound)?,
Value::Array(arr) => {
let idx = token
.parse::<usize>()
.map_err(|_| PatchError::InvalidPath)?;
arr.get_mut(idx).ok_or(PatchError::PathNotFound)?
}
_ => return Err(PatchError::InvalidPath),
};
}
Err(PatchError::PathNotFound)
}
pub fn replace(&self, doc: &mut Value, value: Value) -> Result<(), PatchError> {
if self.tokens.is_empty() {
*doc = value;
return Ok(());
}
let mut current = doc;
let last = self.tokens.len() - 1;
for (i, token) in self.tokens.iter().enumerate() {
if i == last {
match current {
Value::Object(map) => {
if !map.contains_key(token) {
return Err(PatchError::PathNotFound);
}
map.insert(token.clone(), value);
}
Value::Array(arr) => {
let idx = token
.parse::<usize>()
.map_err(|_| PatchError::InvalidPath)?;
if idx >= arr.len() {
return Err(PatchError::PathNotFound);
}
arr[idx] = value;
}
_ => return Err(PatchError::InvalidPath),
}
return Ok(());
}
current = match current {
Value::Object(map) => map.get_mut(token).ok_or(PatchError::PathNotFound)?,
Value::Array(arr) => {
let idx = token
.parse::<usize>()
.map_err(|_| PatchError::InvalidPath)?;
arr.get_mut(idx).ok_or(PatchError::PathNotFound)?
}
_ => return Err(PatchError::InvalidPath),
};
}
Ok(())
}
}
}
pub mod utils {
use super::*;
pub fn is_empty_patch(patch: &JsonPatch) -> bool {
patch.is_empty()
}
pub fn patch_size(patch: &JsonPatch) -> usize {
serde_json::to_string(patch).map(|s| s.len()).unwrap_or(0)
}
pub fn should_use_patch(_old: &Value, new: &Value, patch: &JsonPatch) -> bool {
let patch_bytes = patch_size(patch);
let full_bytes = serde_json::to_string(new)
.map(|s| s.len())
.unwrap_or(usize::MAX);
patch_bytes < (full_bytes * 8 / 10)
}
pub fn create_partial_patch(old: &Value, new: &Value, fields: &[&str]) -> JsonPatch {
let full_patch = compute_diff(old, new);
full_patch
.into_iter()
.filter(|op| {
let path = match op {
PatchOperation::Add { path, .. } => path,
PatchOperation::Remove { path } => path,
PatchOperation::Replace { path, .. } => path,
PatchOperation::Copy { path, .. } => path,
PatchOperation::Move { path, .. } => path,
PatchOperation::Test { path, .. } => path,
};
fields.iter().any(|field| {
let field_path = format!("/{}", field);
path == &field_path || path.starts_with(&format!("{}/", field_path))
})
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compute_diff_add_field() {
let old = serde_json::json!({"name": "Alice"});
let new = serde_json::json!({"name": "Alice", "age": 30});
let patch = compute_diff(&old, &new);
assert_eq!(patch.len(), 1);
assert!(matches!(&patch[0], PatchOperation::Add { path, value }
if path == "/age" && value == &serde_json::json!(30)));
}
#[test]
fn test_compute_diff_remove_field() {
let old = serde_json::json!({"name": "Alice", "age": 30});
let new = serde_json::json!({"name": "Alice"});
let patch = compute_diff(&old, &new);
assert_eq!(patch.len(), 1);
assert!(matches!(&patch[0], PatchOperation::Remove { path }
if path == "/age"));
}
#[test]
fn test_compute_diff_replace_field() {
let old = serde_json::json!({"name": "Alice", "age": 30});
let new = serde_json::json!({"name": "Alice", "age": 31});
let patch = compute_diff(&old, &new);
assert_eq!(patch.len(), 1);
assert!(matches!(&patch[0], PatchOperation::Replace { path, value }
if path == "/age" && value == &serde_json::json!(31)));
}
#[test]
fn test_apply_patch_add() {
let mut doc = serde_json::json!({"name": "Alice"});
let patch = vec![PatchOperation::Add {
path: "/age".to_string(),
value: serde_json::json!(30),
}];
apply_patch(&mut doc, &patch).unwrap();
assert_eq!(doc["age"], 30);
}
#[test]
fn test_apply_patch_replace() {
let mut doc = serde_json::json!({"name": "Alice", "age": 30});
let patch = vec![PatchOperation::Replace {
path: "/age".to_string(),
value: serde_json::json!(31),
}];
apply_patch(&mut doc, &patch).unwrap();
assert_eq!(doc["age"], 31);
}
#[test]
fn test_apply_patch_remove() {
let mut doc = serde_json::json!({"name": "Alice", "age": 30});
let patch = vec![PatchOperation::Remove {
path: "/age".to_string(),
}];
apply_patch(&mut doc, &patch).unwrap();
assert!(doc.get("age").is_none());
}
#[test]
fn test_nested_diff() {
let old = serde_json::json!({
"user": {"name": "Alice", "age": 30}
});
let new = serde_json::json!({
"user": {"name": "Alice", "age": 31}
});
let patch = compute_diff(&old, &new);
assert!(patch.iter().any(|op| matches!(op,
PatchOperation::Replace { path, .. } if path == "/user/age"
)));
}
#[test]
fn test_json_pointer_escape() {
let old = serde_json::json!({});
let new = serde_json::json!({"foo/bar": "value"});
let patch = compute_diff(&old, &new);
assert!(patch.iter().any(|op| matches!(op,
PatchOperation::Add { path, .. } if path == "/foo~1bar"
)));
}
#[test]
fn test_should_use_patch() {
let old = serde_json::json!({
"large_field": "x".repeat(1000),
"small_field": "y"
});
let new = serde_json::json!({
"large_field": "x".repeat(1000),
"small_field": "z"
});
let patch = compute_diff(&old, &new);
assert!(utils::should_use_patch(&old, &new, &patch));
}
#[test]
fn test_array_diff_element_change() {
let old = serde_json::json!({"items": [1, 2, 3]});
let new = serde_json::json!({"items": [1, 5, 3]});
let patch = compute_diff(&old, &new);
assert!(patch.iter().any(|op| matches!(op,
PatchOperation::Replace { path, value } if path == "/items/1" && value == &serde_json::json!(5)
)));
}
#[test]
fn test_array_diff_add_element() {
let old = serde_json::json!({"items": [1, 2]});
let new = serde_json::json!({"items": [1, 2, 3]});
let patch = compute_diff(&old, &new);
assert!(patch.iter().any(|op| matches!(op,
PatchOperation::Add { path, value } if path == "/items/-" && value == &serde_json::json!(3)
)));
}
#[test]
fn test_array_diff_remove_element() {
let old = serde_json::json!({"items": [1, 2, 3]});
let new = serde_json::json!({"items": [1, 2]});
let patch = compute_diff(&old, &new);
assert!(patch.iter().any(|op| matches!(op,
PatchOperation::Remove { path } if path == "/items/2"
)));
}
#[test]
fn test_array_diff_large_change_replaces() {
let old = serde_json::json!({"items": [1, 2]});
let new = serde_json::json!({"items": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]});
let patch = compute_diff(&old, &new);
assert!(patch.iter().any(|op| matches!(op,
PatchOperation::Replace { path, .. } if path == "/items"
)));
}
#[test]
fn test_array_diff_nested_objects() {
let old = serde_json::json!({
"users": [
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25}
]
});
let new = serde_json::json!({
"users": [
{"name": "Alice", "age": 31},
{"name": "Bob", "age": 25}
]
});
let patch = compute_diff(&old, &new);
assert!(patch.iter().any(|op| matches!(op,
PatchOperation::Replace { path, value } if path == "/users/0/age" && value == &serde_json::json!(31)
)));
}
}