fionn_diff/
merge.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! JSON Merge Patch implementation (RFC 7396).
3//!
4//! A simpler alternative to JSON Patch where:
5//! - Objects are recursively merged
6//! - `null` values indicate deletion
7//! - Other values replace existing ones
8//!
9//! # Example
10//!
11//! ```ignore
12//! use fionn::diff::{json_merge_patch, merge_patch_to_value};
13//! use serde_json::json;
14//!
15//! let original = json!({
16//!     "title": "Hello",
17//!     "author": {"name": "John"},
18//!     "content": "..."
19//! });
20//!
21//! let patch = json!({
22//!     "title": "New Title",
23//!     "author": {"name": null},  // Delete author.name
24//!     "published": true          // Add new field
25//! });
26//!
27//! let result = json_merge_patch(&original, &patch);
28//! // result = {"title": "New Title", "author": {}, "content": "...", "published": true}
29//! ```
30
31use serde_json::Value;
32
33/// Apply a JSON Merge Patch (RFC 7396) to a document.
34///
35/// # Arguments
36///
37/// * `target` - The original JSON document
38/// * `patch` - The merge patch to apply
39///
40/// # Returns
41///
42/// A new JSON document with the patch applied.
43///
44/// # Rules
45///
46/// - If `patch` is not an object, it replaces `target` entirely
47/// - If `patch` is an object:
48///   - For each key in `patch`:
49///     - If value is `null`, remove the key from `target`
50///     - If value is an object and target has an object at that key, merge recursively
51///     - Otherwise, set the key to the patch value
52#[must_use]
53pub fn json_merge_patch(target: &Value, patch: &Value) -> Value {
54    let mut result = target.clone();
55    merge_patch_mut(&mut result, patch);
56    result
57}
58
59/// Apply a JSON Merge Patch in place.
60pub fn merge_patch_mut(target: &mut Value, patch: &Value) {
61    // If patch is not an object, it replaces the target entirely
62    if !patch.is_object() {
63        *target = patch.clone();
64        return;
65    }
66
67    // Ensure target is an object (or convert it to one)
68    if !target.is_object() {
69        *target = Value::Object(serde_json::Map::new());
70    }
71
72    let target_obj = target.as_object_mut().expect("target should be object");
73    let patch_obj = patch.as_object().expect("patch should be object");
74
75    for (key, patch_value) in patch_obj {
76        if patch_value.is_null() {
77            // Null means delete
78            target_obj.remove(key);
79        } else if patch_value.is_object() {
80            // Recursive merge for objects
81            let target_value = target_obj
82                .entry(key.clone())
83                .or_insert_with(|| Value::Object(serde_json::Map::new()));
84            merge_patch_mut(target_value, patch_value);
85        } else {
86            // Replace/add for non-objects
87            target_obj.insert(key.clone(), patch_value.clone());
88        }
89    }
90}
91
92/// Generate a merge patch from two JSON documents.
93///
94/// Creates a patch that, when applied to `source`, produces `target`.
95///
96/// # Returns
97///
98/// A JSON value representing the merge patch.
99#[must_use]
100pub fn merge_patch_to_value(source: &Value, target: &Value) -> Value {
101    generate_merge_patch(source, target)
102}
103
104/// Generate a merge patch between two values.
105fn generate_merge_patch(source: &Value, target: &Value) -> Value {
106    match (source, target) {
107        // Both objects - generate object diff
108        (Value::Object(src), Value::Object(tgt)) => {
109            let mut patch = serde_json::Map::new();
110
111            // Find removed keys (set to null)
112            for key in src.keys() {
113                if !tgt.contains_key(key) {
114                    patch.insert(key.clone(), Value::Null);
115                }
116            }
117
118            // Find added and modified keys
119            for (key, tgt_value) in tgt {
120                match src.get(key) {
121                    Some(src_value) => {
122                        // Key exists in both - check if different
123                        if src_value != tgt_value {
124                            let nested_patch = generate_merge_patch(src_value, tgt_value);
125                            // Only include if there's actual change
126                            if nested_patch != Value::Object(serde_json::Map::new()) {
127                                patch.insert(key.clone(), nested_patch);
128                            }
129                        }
130                    }
131                    None => {
132                        // Key is new
133                        patch.insert(key.clone(), tgt_value.clone());
134                    }
135                }
136            }
137
138            Value::Object(patch)
139        }
140
141        // Different types or source is not object - full replacement
142        _ => {
143            if source == target {
144                Value::Object(serde_json::Map::new()) // Empty patch = no change
145            } else {
146                target.clone()
147            }
148        }
149    }
150}
151
152/// Merge multiple JSON documents using merge patch semantics.
153///
154/// Documents are merged left to right, with later documents taking precedence.
155#[must_use]
156pub fn merge_many(documents: &[Value]) -> Value {
157    documents
158        .iter()
159        .fold(Value::Object(serde_json::Map::new()), |acc, doc| {
160            json_merge_patch(&acc, doc)
161        })
162}
163
164/// Deep merge two objects, preferring values from `overlay`.
165///
166/// Unlike merge patch, this doesn't treat `null` as deletion.
167#[must_use]
168pub fn deep_merge(base: &Value, overlay: &Value) -> Value {
169    match (base, overlay) {
170        (Value::Object(base_obj), Value::Object(overlay_obj)) => {
171            let mut result = base_obj.clone();
172
173            for (key, overlay_value) in overlay_obj {
174                let merged = result.get(key).map_or_else(
175                    || overlay_value.clone(),
176                    |base_value| deep_merge(base_value, overlay_value),
177                );
178                result.insert(key.clone(), merged);
179            }
180
181            Value::Object(result)
182        }
183        // Non-objects: overlay wins
184        (_, overlay) => overlay.clone(),
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use serde_json::json;
192
193    #[test]
194    fn test_merge_patch_add_field() {
195        let target = json!({"a": "b"});
196        let patch = json!({"c": "d"});
197        let result = json_merge_patch(&target, &patch);
198        assert_eq!(result, json!({"a": "b", "c": "d"}));
199    }
200
201    #[test]
202    fn test_merge_patch_replace_field() {
203        let target = json!({"a": "b"});
204        let patch = json!({"a": "c"});
205        let result = json_merge_patch(&target, &patch);
206        assert_eq!(result, json!({"a": "c"}));
207    }
208
209    #[test]
210    fn test_merge_patch_delete_field() {
211        let target = json!({"a": "b", "c": "d"});
212        let patch = json!({"a": null});
213        let result = json_merge_patch(&target, &patch);
214        assert_eq!(result, json!({"c": "d"}));
215    }
216
217    #[test]
218    fn test_merge_patch_nested() {
219        let target = json!({
220            "title": "Hello",
221            "author": {
222                "name": "John",
223                "email": "john@example.com"
224            }
225        });
226        let patch = json!({
227            "title": "New Title",
228            "author": {
229                "email": null,
230                "twitter": "@john"
231            }
232        });
233        let result = json_merge_patch(&target, &patch);
234        assert_eq!(
235            result,
236            json!({
237                "title": "New Title",
238                "author": {
239                    "name": "John",
240                    "twitter": "@john"
241                }
242            })
243        );
244    }
245
246    #[test]
247    fn test_merge_patch_replace_object_with_value() {
248        let target = json!({"a": {"b": "c"}});
249        let patch = json!({"a": "value"});
250        let result = json_merge_patch(&target, &patch);
251        assert_eq!(result, json!({"a": "value"}));
252    }
253
254    #[test]
255    fn test_merge_patch_replace_value_with_object() {
256        let target = json!({"a": "value"});
257        let patch = json!({"a": {"b": "c"}});
258        let result = json_merge_patch(&target, &patch);
259        assert_eq!(result, json!({"a": {"b": "c"}}));
260    }
261
262    #[test]
263    fn test_merge_patch_array_replacement() {
264        // Arrays are replaced entirely, not merged
265        let target = json!({"arr": [1, 2, 3]});
266        let patch = json!({"arr": [4, 5]});
267        let result = json_merge_patch(&target, &patch);
268        assert_eq!(result, json!({"arr": [4, 5]}));
269    }
270
271    #[test]
272    fn test_merge_patch_non_object_patch() {
273        let target = json!({"a": "b"});
274        let patch = json!("string");
275        let result = json_merge_patch(&target, &patch);
276        assert_eq!(result, json!("string"));
277    }
278
279    #[test]
280    fn test_merge_patch_to_non_object_target() {
281        let target = json!("string");
282        let patch = json!({"a": "b"});
283        let result = json_merge_patch(&target, &patch);
284        assert_eq!(result, json!({"a": "b"}));
285    }
286
287    #[test]
288    fn test_generate_merge_patch_add() {
289        let source = json!({"a": "b"});
290        let target = json!({"a": "b", "c": "d"});
291        let patch = merge_patch_to_value(&source, &target);
292        assert_eq!(patch, json!({"c": "d"}));
293    }
294
295    #[test]
296    fn test_generate_merge_patch_remove() {
297        let source = json!({"a": "b", "c": "d"});
298        let target = json!({"a": "b"});
299        let patch = merge_patch_to_value(&source, &target);
300        assert_eq!(patch, json!({"c": null}));
301    }
302
303    #[test]
304    fn test_generate_merge_patch_modify() {
305        let source = json!({"a": "b"});
306        let target = json!({"a": "c"});
307        let patch = merge_patch_to_value(&source, &target);
308        assert_eq!(patch, json!({"a": "c"}));
309    }
310
311    #[test]
312    fn test_generate_merge_patch_nested() {
313        let source = json!({"outer": {"a": 1, "b": 2}});
314        let target = json!({"outer": {"a": 1, "c": 3}});
315        let patch = merge_patch_to_value(&source, &target);
316        assert_eq!(patch, json!({"outer": {"b": null, "c": 3}}));
317    }
318
319    #[test]
320    fn test_generate_merge_patch_identical() {
321        let source = json!({"a": "b", "c": [1, 2, 3]});
322        let patch = merge_patch_to_value(&source, &source);
323        assert_eq!(patch, json!({}));
324    }
325
326    #[test]
327    fn test_roundtrip() {
328        let source = json!({
329            "name": "Test",
330            "value": 42,
331            "nested": {"a": 1, "b": 2},
332            "arr": [1, 2, 3]
333        });
334
335        let target = json!({
336            "name": "Modified",
337            "nested": {"a": 1, "c": 3},
338            "arr": [4, 5],
339            "new_field": true
340        });
341
342        let patch = merge_patch_to_value(&source, &target);
343        let result = json_merge_patch(&source, &patch);
344        assert_eq!(result, target);
345    }
346
347    #[test]
348    fn test_merge_many() {
349        let docs = vec![json!({"a": 1}), json!({"b": 2}), json!({"a": 10, "c": 3})];
350        let result = merge_many(&docs);
351        assert_eq!(result, json!({"a": 10, "b": 2, "c": 3}));
352    }
353
354    #[test]
355    fn test_merge_many_empty() {
356        let docs: Vec<Value> = vec![];
357        let result = merge_many(&docs);
358        assert_eq!(result, json!({}));
359    }
360
361    #[test]
362    fn test_deep_merge() {
363        let base = json!({
364            "a": 1,
365            "nested": {"x": 10, "y": 20}
366        });
367        let overlay = json!({
368            "b": 2,
369            "nested": {"y": 200, "z": 30}
370        });
371        let result = deep_merge(&base, &overlay);
372        assert_eq!(
373            result,
374            json!({
375                "a": 1,
376                "b": 2,
377                "nested": {"x": 10, "y": 200, "z": 30}
378            })
379        );
380    }
381
382    #[test]
383    fn test_deep_merge_null_preserved() {
384        // Unlike merge patch, deep_merge preserves nulls
385        let base = json!({"a": 1});
386        let overlay = json!({"a": null});
387        let result = deep_merge(&base, &overlay);
388        assert_eq!(result, json!({"a": null}));
389    }
390
391    #[test]
392    fn test_rfc7396_examples() {
393        // Examples from RFC 7396
394
395        // Example 1
396        let target = json!({"a": "b"});
397        let patch = json!({"a": "c"});
398        assert_eq!(json_merge_patch(&target, &patch), json!({"a": "c"}));
399
400        // Example 2
401        let target = json!({"a": "b"});
402        let patch = json!({"b": "c"});
403        assert_eq!(
404            json_merge_patch(&target, &patch),
405            json!({"a": "b", "b": "c"})
406        );
407
408        // Example 3
409        let target = json!({"a": "b"});
410        let patch = json!({"a": null});
411        assert_eq!(json_merge_patch(&target, &patch), json!({}));
412
413        // Example 4
414        let target = json!({"a": "b", "b": "c"});
415        let patch = json!({"a": null});
416        assert_eq!(json_merge_patch(&target, &patch), json!({"b": "c"}));
417
418        // Example 5
419        let target = json!({"a": ["b"]});
420        let patch = json!({"a": "c"});
421        assert_eq!(json_merge_patch(&target, &patch), json!({"a": "c"}));
422
423        // Example 6
424        let target = json!({"a": "c"});
425        let patch = json!({"a": ["b"]});
426        assert_eq!(json_merge_patch(&target, &patch), json!({"a": ["b"]}));
427
428        // Example 7
429        let target = json!({"a": {"b": "c"}});
430        let patch = json!({"a": {"b": "d", "c": null}});
431        assert_eq!(json_merge_patch(&target, &patch), json!({"a": {"b": "d"}}));
432
433        // Example 8
434        let target = json!({"a": [{"b": "c"}]});
435        let patch = json!({"a": [1]});
436        assert_eq!(json_merge_patch(&target, &patch), json!({"a": [1]}));
437    }
438}