From 0dd3d5e1048fc207106d92517c36bac0b8e7bc92 Mon Sep 17 00:00:00 2001 From: Alexander Onnikov Date: Wed, 24 Sep 2025 23:48:11 +0700 Subject: [PATCH] add huly remove operation Signed-off-by: Alexander Onnikov --- server/src/patch.rs | 86 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/server/src/patch.rs b/server/src/patch.rs index 36711db7ee..9df966dd34 100644 --- a/server/src/patch.rs +++ b/server/src/patch.rs @@ -30,6 +30,17 @@ pub struct IncOperationExt { pub safe: bool, } +/// 'remove' operation - increments a numeric value +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct RemoveOperationExt { + /// JSON-Pointer value [RFC6901](https://tools.ietf.org/html/rfc6901) that references a location + /// within the target document where the operation is performed. + pub path: PointerBuf, + // When enabled, ensures that the operation does not create new fields + #[serde(default)] + pub safe: bool, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "hop")] #[serde(rename_all = "lowercase")] @@ -38,6 +49,8 @@ pub enum HulyPatchOperation { Add(AddOperationExt), /// 'inc' operation Inc(IncOperationExt), + /// 'remove' operation + Remove(RemoveOperationExt), } #[derive(Debug, Clone, PartialEq, Eq)] @@ -84,6 +97,7 @@ pub fn apply(doc: &mut Value, patches: &[PatchOperation]) -> Result<(), HulyPatc PatchOperation::Huly(huly_op) => match huly_op { HulyPatchOperation::Add(op) => add(doc, &op.path, &op.value, op.safe), HulyPatchOperation::Inc(op) => inc(doc, &op.path, &op.value, op.safe), + HulyPatchOperation::Remove(op) => remove(doc, &op.path, op.safe), }, PatchOperation::Standard(standard_op) => Ok(Some(standard_op.clone())), }? { @@ -149,6 +163,24 @@ fn inc( } } +fn remove( + doc: &Value, + path: &Pointer, + safe: bool, +) -> Result, HulyPatchError> { + let target = doc.pointer(path.as_str()); + + Ok(if safe && target.is_none() { + None + } else { + Some(StandardPatchOperation::Remove( + json_patch::RemoveOperation { + path: path.to_owned(), + }, + )) + }) +} + fn add_json_numbers(a: &Number, b: &Number) -> Result { a.as_i64() .and_then(|a| b.as_i64().map(|b| Number::from(a + b))) @@ -210,6 +242,27 @@ mod tests { } } + fn test_remove( + doc: Value, + path: PointerBuf, + safe: bool, + expected: Result, HulyPatchError>, + ) { + let mut doc = doc.clone(); + let res = remove(&mut doc, &path, safe); + + match expected { + Ok(op) => { + assert!(res.is_ok()); + assert_eq!(op, res.unwrap()); + } + Err(e) => { + assert!(res.is_err()); + assert_eq!(Some(e), res.err()); + } + } + } + #[test] fn test_add_non_existing_object_field() { test_add( @@ -409,6 +462,39 @@ mod tests { ); } + #[test] + fn test_remove_existing_object_field() { + test_remove( + json!({ "a": 1 }), + PointerBuf::from_tokens(["a"]), + false, + Ok(Some(StandardPatchOperation::Remove( + json_patch::RemoveOperation { + path: PointerBuf::from_tokens(["a"]), + }, + ))), + ); + } + + #[test] + fn test_remove_non_existing_object_field() { + test_remove( + json!({}), + PointerBuf::from_tokens(["a"]), + false, + Ok(Some(StandardPatchOperation::Remove( + json_patch::RemoveOperation { + path: PointerBuf::from_tokens(["a"]), + }, + ))), + ); + } + + #[test] + fn test_remove_non_existing_object_field_safe() { + test_remove(json!({}), PointerBuf::from_tokens(["a"]), true, Ok(None)); + } + #[test] fn test_add_json_numbers_i_i() { let res = add_json_numbers(&Number::from(1), &Number::from(1));