From 846a73188af3349cf4c087978d4df91e5f2f208a Mon Sep 17 00:00:00 2001 From: Ihor Andrianov Date: Tue, 28 Jan 2025 20:00:15 +0200 Subject: [PATCH] add json_patch implementation --- core/json/json_operations.rs | 103 +++++++++++++++++++++++++++++++++++ core/json/mod.rs | 2 + 2 files changed, 105 insertions(+) create mode 100644 core/json/json_operations.rs diff --git a/core/json/json_operations.rs b/core/json/json_operations.rs new file mode 100644 index 000000000..6a91e157a --- /dev/null +++ b/core/json/json_operations.rs @@ -0,0 +1,103 @@ +use std::collections::VecDeque; + +use crate::types::OwnedValue; + +use super::{convert_json_to_db_type, get_json_value, Val}; + +/// Represents a single patch operation in the merge queue. +/// +/// Used internally by the `merge_patch` function to track the path and value +/// for each pending merge operation. +#[derive(Debug, Clone)] +struct PatchOperation { + path: Vec, + patch: Val, +} + +/// The function follows RFC 7386 JSON Merge Patch semantics: +/// * If the patch is null, the target is replaced with null +/// * If the patch contains a scalar value, the target is replaced with that value +/// * If both target and patch are objects, the patch is recursively applied +/// * null values in the patch result in property removal from the target +pub fn json_patch(target: &OwnedValue, patch: &OwnedValue) -> crate::Result { + match (target, patch) { + (OwnedValue::Blob(_), _) | (_, OwnedValue::Blob(_)) => { + crate::bail_constraint_error!("blob is not supported!"); + } + _ => (), + } + + let mut parsed_target = get_json_value(target)?; + let parsed_patch = get_json_value(patch)?; + + merge_patch(&mut parsed_target, parsed_patch); + + convert_json_to_db_type(&parsed_target, false) +} + +/// # Implementation Notes +/// +/// * Processes patches in breadth-first order +/// * Maintains path information for nested updates +/// * Handles object merging with proper null value semantics +/// * Preserves existing values not mentioned in the patch +/// +/// Following RFC 7386 rules: +/// * null values remove properties +/// * Objects are merged recursively +/// * Non-object values replace the target completely +fn merge_patch(target: &mut Val, patch: Val) { + let mut queue = VecDeque::with_capacity(8); + + queue.push_back(PatchOperation { + path: Vec::new(), + patch, + }); + while let Some(PatchOperation { path, patch }) = queue.pop_front() { + let mut current: &mut Val = target; + + for (depth, key) in path.iter().enumerate() { + if let Val::Object(ref mut obj) = current { + current = &mut obj + .get_mut(*key) + .unwrap_or_else(|| { + panic!("Invalid path at depth {}: key '{}' not found", depth, key) + }) + .1; + } + } + + match (current, patch) { + (curr, Val::Null) => { + *curr = Val::Null; + } + (Val::Object(target_map), Val::Object(patch_map)) => { + for (key, patch_val) in patch_map { + if let Some(pos) = target_map + .iter() + .position(|(target_key, _)| target_key == &key) + { + match patch_val { + Val::Null => { + target_map.remove(pos); + } + val => { + let mut new_path = path.clone(); + new_path.push(pos); + queue.push_back(PatchOperation { + path: new_path, + patch: val, + }); + } + }; + } else { + target_map.push((key, patch_val)); + }; + } + } + (curr, patch_val) => { + *curr = patch_val; + } + } + } +} diff --git a/core/json/mod.rs b/core/json/mod.rs index 8b271c8e1..65e90697f 100644 --- a/core/json/mod.rs +++ b/core/json/mod.rs @@ -1,5 +1,6 @@ mod de; mod error; +mod json_operations; mod json_path; mod ser; @@ -8,6 +9,7 @@ use std::rc::Rc; pub use crate::json::de::from_str; use crate::json::de::ordered_object; use crate::json::error::Error as JsonError; +pub use crate::json::json_operations::json_patch; use crate::json::json_path::{json_path, JsonPath, PathElement}; pub use crate::json::ser::to_string; use crate::types::{LimboText, OwnedValue, TextSubtype};