//! RFC 6902 JSON Patch support. use crate::PatchError; use serde::{Deserialize, Serialize}; use serde_json::Value; /// One RFC 6902 JSON Patch operation. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "op", rename_all = "lowercase")] pub enum JsonPatchOperation { /// Add a value at the target JSON Pointer. Add { /// Target JSON Pointer. path: String, /// Value to insert. value: Value, }, /// Remove the value at the target JSON Pointer. Remove { /// Target JSON Pointer. path: String, }, /// Replace the value at the target JSON Pointer. Replace { /// Target JSON Pointer. path: String, /// Replacement value. value: Value, }, /// Move a value from one JSON Pointer to another. Move { /// Source JSON Pointer. from: String, /// Target JSON Pointer. path: String, }, /// Copy a value from one JSON Pointer to another. Copy { /// Source JSON Pointer. from: String, /// Target JSON Pointer. path: String, }, /// Assert that a JSON Pointer currently contains a value. Test { /// Target JSON Pointer. path: String, /// Expected value. value: Value, }, } /// Applies an RFC 6902 JSON Patch document to a JSON document string. pub fn apply_json_patch(doc: &str, patch: &str) -> crate::Result { let mut document: Value = serde_json::from_str(doc) .map_err(|error| PatchError::ApplyFailed(format!("invalid JSON document: {error}")))?; let operations: Vec = serde_json::from_str(patch) .map_err(|error| PatchError::ApplyFailed(format!("invalid JSON patch: {error}")))?; apply_json_patch_value(&mut document, &operations)?; serde_json::to_string(&document) .map_err(|error| PatchError::ApplyFailed(format!("failed to serialize JSON: {error}"))) } /// Applies parsed JSON Patch operations to a JSON value. /// /// Each operation is applied atomically: when one operation fails, the document /// remains at the state produced by the previous successful operation. pub fn apply_json_patch_value( document: &mut Value, operations: &[JsonPatchOperation], ) -> crate::Result<()> { for operation in operations { let mut next = document.clone(); apply_operation(&mut next, operation)?; *document = next; } Ok(()) } fn apply_operation(document: &mut Value, operation: &JsonPatchOperation) -> crate::Result<()> { match operation { JsonPatchOperation::Add { path, value } => add_value(document, path, value.clone()), JsonPatchOperation::Remove { path } => remove_value(document, path).map(drop), JsonPatchOperation::Replace { path, value } => replace_value(document, path, value.clone()), JsonPatchOperation::Move { from, path } => { if from == path { return Ok(()); } let value = get_value(document, from)?.clone(); remove_value(document, from)?; add_value(document, path, value) } JsonPatchOperation::Copy { from, path } => { let value = get_value(document, from)?.clone(); add_value(document, path, value) } JsonPatchOperation::Test { path, value } => { let actual = get_value(document, path)?; if actual == value { Ok(()) } else { Err(failed(format!( "JSON patch test failed at {path}: expected {value}, actual {actual}" ))) } } } } fn add_value(document: &mut Value, path: &str, value: Value) -> crate::Result<()> { let tokens = parse_json_pointer(path)?; if tokens.is_empty() { *document = value; return Ok(()); } let key = tokens.last().expect("checked non-empty").clone(); let parent = get_mut_by_tokens(document, &tokens[..tokens.len() - 1])?; match parent { Value::Object(map) => { map.insert(key, value); Ok(()) } Value::Array(items) => { if key == "-" { items.push(value); return Ok(()); } let index = parse_array_index(&key, items.len(), true)?; items.insert(index, value); Ok(()) } other => Err(failed(format!( "cannot add JSON patch value below non-container value {other}" ))), } } fn remove_value(document: &mut Value, path: &str) -> crate::Result { let tokens = parse_json_pointer(path)?; if tokens.is_empty() { return Ok(std::mem::take(document)); } let key = tokens.last().expect("checked non-empty").clone(); let parent = get_mut_by_tokens(document, &tokens[..tokens.len() - 1])?; match parent { Value::Object(map) => map .remove(&key) .ok_or_else(|| failed(format!("JSON patch path {path} does not exist"))), Value::Array(items) => { let index = parse_array_index(&key, items.len(), false)?; Ok(items.remove(index)) } other => Err(failed(format!( "cannot remove JSON patch value below non-container value {other}" ))), } } fn replace_value(document: &mut Value, path: &str, value: Value) -> crate::Result<()> { let tokens = parse_json_pointer(path)?; if tokens.is_empty() { *document = value; return Ok(()); } let key = tokens.last().expect("checked non-empty").clone(); let parent = get_mut_by_tokens(document, &tokens[..tokens.len() - 1])?; match parent { Value::Object(map) => { let slot = map .get_mut(&key) .ok_or_else(|| failed(format!("JSON patch path {path} does not exist")))?; *slot = value; Ok(()) } Value::Array(items) => { let index = parse_array_index(&key, items.len(), false)?; items[index] = value; Ok(()) } other => Err(failed(format!( "cannot replace JSON patch value below non-container value {other}" ))), } } fn get_value<'a>(document: &'a Value, path: &str) -> crate::Result<&'a Value> { let tokens = parse_json_pointer(path)?; let mut current = document; for token in &tokens { current = match current { Value::Object(map) => map .get(token) .ok_or_else(|| failed(format!("JSON patch path {path} does not exist")))?, Value::Array(items) => { let index = parse_array_index(token, items.len(), false)?; items .get(index) .ok_or_else(|| failed(format!("JSON patch path {path} does not exist")))? } other => { return Err(failed(format!( "cannot traverse JSON patch path {path} through non-container value {other}" ))) } }; } Ok(current) } fn get_mut_by_tokens<'a>( document: &'a mut Value, tokens: &[String], ) -> crate::Result<&'a mut Value> { let mut current = document; for token in tokens { current = match current { Value::Object(map) => map .get_mut(token) .ok_or_else(|| failed(format!("JSON patch path segment {token} does not exist")))?, Value::Array(items) => { let index = parse_array_index(token, items.len(), false)?; items.get_mut(index).ok_or_else(|| { failed(format!("JSON patch path segment {token} does not exist")) })? } other => { return Err(failed(format!( "cannot traverse JSON patch path through non-container value {other}" ))) } }; } Ok(current) } fn parse_array_index(token: &str, len: usize, allow_end: bool) -> crate::Result { if token.is_empty() || token == "-" { return Err(failed(format!("invalid JSON patch array index {token}"))); } let index = token .parse::() .map_err(|_| failed(format!("invalid JSON patch array index {token}")))?; let max = if allow_end { len } else { len.checked_sub(1) .ok_or_else(|| failed("JSON patch array index exceeds empty array".to_string()))? }; if index > max { return Err(failed(format!( "JSON patch array index {index} exceeds length {len}" ))); } Ok(index) } fn parse_json_pointer(pointer: &str) -> crate::Result> { if pointer.is_empty() { return Ok(Vec::new()); } if !pointer.starts_with('/') { return Err(failed(format!( "JSON patch pointer must be empty or start with '/': {pointer}" ))); } pointer[1..] .split('/') .map(decode_json_pointer_token) .collect() } fn decode_json_pointer_token(token: &str) -> crate::Result { let mut decoded = String::with_capacity(token.len()); let mut chars = token.chars(); while let Some(character) = chars.next() { if character != '~' { decoded.push(character); continue; } match chars.next() { Some('0') => decoded.push('~'), Some('1') => decoded.push('/'), Some(other) => { return Err(failed(format!( "invalid JSON patch pointer escape ~{other}" ))) } None => return Err(failed("invalid JSON patch pointer escape ~".to_string())), } } Ok(decoded) } fn failed(message: String) -> PatchError { PatchError::ApplyFailed(message) } #[cfg(test)] mod tests { use super::*; use serde_json::json; #[test] fn apply_json_patch_handles_all_core_operations() { let document = r#"{"name":"alice","items":["a","b"],"meta":{"keep":true}}"#; let patch = r#"[ {"op":"test","path":"/meta/keep","value":true}, {"op":"add","path":"/items/-","value":"c"}, {"op":"replace","path":"/name","value":"bob"}, {"op":"copy","from":"/meta","path":"/copied"}, {"op":"move","from":"/items/0","path":"/first"}, {"op":"remove","path":"/meta/keep"} ]"#; let output = apply_json_patch(document, patch).unwrap(); let value: Value = serde_json::from_str(&output).unwrap(); assert_eq!(value["name"], json!("bob")); assert_eq!(value["items"], json!(["b", "c"])); assert_eq!(value["first"], json!("a")); assert_eq!(value["copied"], json!({"keep": true})); assert_eq!(value["meta"], json!({})); } #[test] fn apply_json_patch_supports_pointer_escapes() { let document = r#"{"a/b":{"tilde~key":1}}"#; let patch = r#"[{"op":"replace","path":"/a~1b/tilde~0key","value":2}]"#; let output = apply_json_patch(document, patch).unwrap(); let value: Value = serde_json::from_str(&output).unwrap(); assert_eq!(value["a/b"]["tilde~key"], json!(2)); } #[test] fn apply_json_patch_rejects_failed_test_without_mutating_value() { let mut value = json!({"enabled": true}); let operations = vec![ JsonPatchOperation::Add { path: "/count".to_string(), value: json!(1), }, JsonPatchOperation::Test { path: "/enabled".to_string(), value: json!(false), }, ]; let error = apply_json_patch_value(&mut value, &operations).unwrap_err(); assert!(matches!(error, crate::PatchError::ApplyFailed(_))); assert_eq!(value, json!({"enabled": true, "count": 1})); } #[test] fn apply_json_patch_rejects_missing_remove_path() { let error = apply_json_patch(r#"{"items":[]}"#, r#"[{"op":"remove","path":"/missing"}]"#) .unwrap_err(); assert!(matches!(error, crate::PatchError::ApplyFailed(_))); } }