diff --git a/src/lib.rs b/src/lib.rs index b9cee55..351bb8b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -423,76 +423,166 @@ fn test(doc: &Value, path: &str, expected: &Value) -> Result<(), PatchErrorKind> /// # } /// ``` pub fn patch(doc: &mut Value, patch: &[PatchOperation]) -> Result<(), PatchError> { - apply_patches(doc, 0, patch) + let mut undo_stack = Vec::with_capacity(patch.len()); + if let Err(e) = apply_patches(doc, patch, Some(&mut undo_stack)) { + if let Err(e) = undo_patches(doc, &undo_stack) { + unreachable!("unable to undo applied patches: {e}") + } + return Err(e); + } + Ok(()) +} + +/// Patch provided JSON document (given as `serde_json::Value`) in-place. Different from [`patch`] +/// if any patch failed, the document is left in an inconsistent state. In case of internal error +/// resulting in panic, document might be left in inconsistent state. +/// +/// # Example +/// Create and patch document: +/// +/// ```rust +/// #[macro_use] +/// use json_patch::{Patch, patch_unsafe}; +/// use serde_json::{from_value, json}; +/// +/// # pub fn main() { +/// let mut doc = json!([ +/// { "name": "Andrew" }, +/// { "name": "Maxim" } +/// ]); +/// +/// let p: Patch = from_value(json!([ +/// { "op": "test", "path": "/0/name", "value": "Andrew" }, +/// { "op": "add", "path": "/0/happy", "value": true } +/// ])).unwrap(); +/// +/// patch_unsafe(&mut doc, &p).unwrap(); +/// assert_eq!(doc, json!([ +/// { "name": "Andrew", "happy": true }, +/// { "name": "Maxim" } +/// ])); +/// +/// # } +/// ``` +pub fn patch_unsafe(doc: &mut Value, patch: &[PatchOperation]) -> Result<(), PatchError> { + apply_patches(doc, patch, None) +} + +/// Undoes operations performed by `apply_patches`. This is useful to recover the original document +/// in case of an error. +fn undo_patches(doc: &mut Value, undo_patches: &[PatchOperation]) -> Result<(), PatchError> { + for (operation, patch) in undo_patches.iter().enumerate().rev() { + match patch { + PatchOperation::Add(op) => { + add(doc, &op.path, op.value.clone()) + .map_err(|e| translate_error(e, operation, &op.path))?; + } + PatchOperation::Remove(op) => { + remove(doc, &op.path, true).map_err(|e| translate_error(e, operation, &op.path))?; + } + PatchOperation::Replace(op) => { + replace(doc, &op.path, op.value.clone()) + .map_err(|e| translate_error(e, operation, &op.path))?; + } + PatchOperation::Move(op) => { + mov(doc, op.from.as_str(), &op.path, true) + .map_err(|e| translate_error(e, operation, &op.path))?; + } + PatchOperation::Copy(op) => { + copy(doc, op.from.as_str(), &op.path) + .map_err(|e| translate_error(e, operation, &op.path))?; + } + _ => unreachable!(), + } + } + + Ok(()) } // Apply patches while tracking all the changes being made so they can be reverted back in case -// subsequent patches fail. Uses stack recursion to keep the state. +// subsequent patches fail. The inverse of all state changes is recorded in the `undo_stack` which +// can be reapplied using `undo_patches` to get back to the original document. fn apply_patches( doc: &mut Value, - operation: usize, patches: &[PatchOperation], + undo_stack: Option<&mut Vec>, ) -> Result<(), PatchError> { - let (patch, tail) = match patches.split_first() { - None => return Ok(()), - Some((patch, tail)) => (patch, tail), - }; - - match *patch { - PatchOperation::Add(ref op) => { - let prev = add(doc, &op.path, op.value.clone()) - .map_err(|e| translate_error(e, operation, &op.path))?; - apply_patches(doc, operation + 1, tail).map_err(move |e| { - match prev { - None => remove(doc, &op.path, true).unwrap(), - Some(v) => add(doc, &op.path, v).unwrap().unwrap(), - }; - e - }) - } - PatchOperation::Remove(ref op) => { - let prev = remove(doc, &op.path, false) - .map_err(|e| translate_error(e, operation, &op.path))?; - apply_patches(doc, operation + 1, tail).map_err(move |e| { - assert!(add(doc, &op.path, prev).unwrap().is_none()); - e - }) - } - PatchOperation::Replace(ref op) => { - let prev = replace(doc, &op.path, op.value.clone()) - .map_err(|e| translate_error(e, operation, &op.path))?; - apply_patches(doc, operation + 1, tail).map_err(move |e| { - replace(doc, &op.path, prev).unwrap(); - e - }) - } - PatchOperation::Move(ref op) => { - let prev = mov(doc, op.from.as_str(), &op.path, false) - .map_err(|e| translate_error(e, operation, &op.path))?; - apply_patches(doc, operation + 1, tail).map_err(move |e| { - mov(doc, &op.path, op.from.as_str(), true).unwrap(); - if let Some(prev) = prev { - assert!(add(doc, &op.path, prev).unwrap().is_none()); + for (operation, patch) in patches.iter().enumerate() { + match patch { + PatchOperation::Add(ref op) => { + let prev = add(doc, &op.path, op.value.clone()) + .map_err(|e| translate_error(e, operation, &op.path))?; + if let Some(&mut ref mut undo_stack) = undo_stack { + undo_stack.push(match prev { + None => PatchOperation::Remove(RemoveOperation { + path: op.path.clone(), + }), + Some(v) => PatchOperation::Add(AddOperation { + path: op.path.clone(), + value: v, + }), + }) } - e - }) - } - PatchOperation::Copy(ref op) => { - let prev = copy(doc, op.from.as_str(), &op.path) - .map_err(|e| translate_error(e, operation, &op.path))?; - apply_patches(doc, operation + 1, tail).map_err(move |e| { - match prev { - None => remove(doc, &op.path, true).unwrap(), - Some(v) => add(doc, &op.path, v).unwrap().unwrap(), - }; - e - }) - } - PatchOperation::Test(ref op) => { - test(doc, &op.path, &op.value).map_err(|e| translate_error(e, operation, &op.path))?; - apply_patches(doc, operation + 1, tail) + } + PatchOperation::Remove(ref op) => { + let prev = remove(doc, &op.path, false) + .map_err(|e| translate_error(e, operation, &op.path))?; + if let Some(&mut ref mut undo_stack) = undo_stack { + undo_stack.push(PatchOperation::Add(AddOperation { + path: op.path.clone(), + value: prev, + })) + } + } + PatchOperation::Replace(ref op) => { + let prev = replace(doc, &op.path, op.value.clone()) + .map_err(|e| translate_error(e, operation, &op.path))?; + if let Some(&mut ref mut undo_stack) = undo_stack { + undo_stack.push(PatchOperation::Replace(ReplaceOperation { + path: op.path.clone(), + value: prev, + })) + } + } + PatchOperation::Move(ref op) => { + let prev = mov(doc, op.from.as_str(), &op.path, false) + .map_err(|e| translate_error(e, operation, &op.path))?; + if let Some(&mut ref mut undo_stack) = undo_stack { + if let Some(prev) = prev { + undo_stack.push(PatchOperation::Add(AddOperation { + path: op.path.clone(), + value: prev, + })); + } + undo_stack.push(PatchOperation::Move(MoveOperation { + from: op.path.clone(), + path: op.from.clone(), + })); + } + } + PatchOperation::Copy(ref op) => { + let prev = copy(doc, op.from.as_str(), &op.path) + .map_err(|e| translate_error(e, operation, &op.path))?; + if let Some(&mut ref mut undo_stack) = undo_stack { + undo_stack.push(match prev { + None => PatchOperation::Remove(RemoveOperation { + path: op.path.clone(), + }), + Some(v) => PatchOperation::Add(AddOperation { + path: op.path.clone(), + value: v, + }), + }) + } + } + PatchOperation::Test(ref op) => { + test(doc, &op.path, &op.value) + .map_err(|e| translate_error(e, operation, &op.path))?; + } } } + + Ok(()) } /// Patch provided JSON document (given as `serde_json::Value`) in place with JSON Merge Patch diff --git a/tests/utoipa.json b/tests/utoipa.json index d726953..385d008 100644 --- a/tests/utoipa.json +++ b/tests/utoipa.json @@ -1 +1 @@ -{"openapi":"3.0.3","info":{"title":"json-patch","description":"RFC 6902, JavaScript Object Notation (JSON) Patch","contact":{"name":"Ivan Dubrov","email":"dubrov.ivan@gmail.com"},"license":{"name":"MIT/Apache-2.0"},"version":"1.0.0"},"paths":{"foo":{"get":{"tags":["crate"],"operationId":"get_foo","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Patch"}}},"required":true},"responses":{"200":{"description":"Patch completed"},"406":{"description":"Not accepted"}}}}},"components":{"schemas":{"AddOperation":{"type":"object","description":"JSON Patch 'add' operation representation","required":["path","value"],"properties":{"path":{"type":"string","description":"JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location\nwithin the target document where the operation is performed."},"value":{"description":"Value to add to the target location."}}},"CopyOperation":{"type":"object","description":"JSON Patch 'copy' operation representation","required":["from","path"],"properties":{"from":{"type":"string","description":"JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location\nto copy value from."},"path":{"type":"string","description":"JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location\nwithin the target document where the operation is performed."}}},"MoveOperation":{"type":"object","description":"JSON Patch 'move' operation representation","required":["from","path"],"properties":{"from":{"type":"string","description":"JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location\nto move value from."},"path":{"type":"string","description":"JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location\nwithin the target document where the operation is performed."}}},"Patch":{"type":"array","items":{"$ref":"#/components/schemas/PatchOperation"}},"PatchOperation":{"oneOf":[{"allOf":[{"$ref":"#/components/schemas/AddOperation"},{"type":"object","required":["op"],"properties":{"op":{"type":"string","enum":["add"]}}}]},{"allOf":[{"$ref":"#/components/schemas/RemoveOperation"},{"type":"object","required":["op"],"properties":{"op":{"type":"string","enum":["remove"]}}}]},{"allOf":[{"$ref":"#/components/schemas/ReplaceOperation"},{"type":"object","required":["op"],"properties":{"op":{"type":"string","enum":["replace"]}}}]},{"allOf":[{"$ref":"#/components/schemas/MoveOperation"},{"type":"object","required":["op"],"properties":{"op":{"type":"string","enum":["move"]}}}]},{"allOf":[{"$ref":"#/components/schemas/CopyOperation"},{"type":"object","required":["op"],"properties":{"op":{"type":"string","enum":["copy"]}}}]},{"allOf":[{"$ref":"#/components/schemas/TestOperation"},{"type":"object","required":["op"],"properties":{"op":{"type":"string","enum":["test"]}}}]}],"description":"JSON Patch single patch operation","discriminator":{"propertyName":"op"}},"RemoveOperation":{"type":"object","description":"JSON Patch 'remove' operation representation","required":["path"],"properties":{"path":{"type":"string","description":"JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location\nwithin the target document where the operation is performed."}}},"ReplaceOperation":{"type":"object","description":"JSON Patch 'replace' operation representation","required":["path","value"],"properties":{"path":{"type":"string","description":"JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location\nwithin the target document where the operation is performed."},"value":{"description":"Value to replace with."}}},"TestOperation":{"type":"object","description":"JSON Patch 'test' operation representation","required":["path","value"],"properties":{"path":{"type":"string","description":"JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location\nwithin the target document where the operation is performed."},"value":{"description":"Value to test against."}}}}}} \ No newline at end of file +{"openapi":"3.0.3","info":{"title":"json-patch","description":"RFC 6902, JavaScript Object Notation (JSON) Patch","contact":{"name":"Ivan Dubrov","email":"dubrov.ivan@gmail.com"},"license":{"name":"MIT/Apache-2.0"},"version":"1.0.0"},"paths":{"foo":{"get":{"tags":["crate"],"operationId":"get_foo","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Patch"}}},"required":true},"responses":{"200":{"description":"Patch completed"},"406":{"description":"Not accepted"}}}}},"components":{"schemas":{"AddOperation":{"type":"object","description":"JSON Patch 'add' operation representation","required":["path","value"],"properties":{"path":{"type":"string","description":"JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location\nwithin the target document where the operation is performed."},"value":{"description":"Value to add to the target location."}}},"CopyOperation":{"type":"object","description":"JSON Patch 'copy' operation representation","required":["from","path"],"properties":{"from":{"type":"string","description":"JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location\nto copy value from."},"path":{"type":"string","description":"JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location\nwithin the target document where the operation is performed."}}},"MoveOperation":{"type":"object","description":"JSON Patch 'move' operation representation","required":["from","path"],"properties":{"from":{"type":"string","description":"JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location\nto move value from."},"path":{"type":"string","description":"JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location\nwithin the target document where the operation is performed."}}},"Patch":{"type":"array","items":{"$ref":"#/components/schemas/PatchOperation"},"description":"Representation of JSON Patch (list of patch operations)"},"PatchOperation":{"oneOf":[{"allOf":[{"$ref":"#/components/schemas/AddOperation"},{"type":"object","required":["op"],"properties":{"op":{"type":"string","enum":["add"]}}}]},{"allOf":[{"$ref":"#/components/schemas/RemoveOperation"},{"type":"object","required":["op"],"properties":{"op":{"type":"string","enum":["remove"]}}}]},{"allOf":[{"$ref":"#/components/schemas/ReplaceOperation"},{"type":"object","required":["op"],"properties":{"op":{"type":"string","enum":["replace"]}}}]},{"allOf":[{"$ref":"#/components/schemas/MoveOperation"},{"type":"object","required":["op"],"properties":{"op":{"type":"string","enum":["move"]}}}]},{"allOf":[{"$ref":"#/components/schemas/CopyOperation"},{"type":"object","required":["op"],"properties":{"op":{"type":"string","enum":["copy"]}}}]},{"allOf":[{"$ref":"#/components/schemas/TestOperation"},{"type":"object","required":["op"],"properties":{"op":{"type":"string","enum":["test"]}}}]}],"description":"JSON Patch single patch operation","discriminator":{"propertyName":"op"}},"RemoveOperation":{"type":"object","description":"JSON Patch 'remove' operation representation","required":["path"],"properties":{"path":{"type":"string","description":"JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location\nwithin the target document where the operation is performed."}}},"ReplaceOperation":{"type":"object","description":"JSON Patch 'replace' operation representation","required":["path","value"],"properties":{"path":{"type":"string","description":"JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location\nwithin the target document where the operation is performed."},"value":{"description":"Value to replace with."}}},"TestOperation":{"type":"object","description":"JSON Patch 'test' operation representation","required":["path","value"],"properties":{"path":{"type":"string","description":"JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location\nwithin the target document where the operation is performed."},"value":{"description":"Value to test against."}}}}}} \ No newline at end of file