fionn_diff/
diff_zerocopy.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Zero-copy diff output using Cow.
3//!
4//! This module provides diff output that minimizes allocations by borrowing
5//! paths when possible and only allocating for computed paths or values.
6
7use serde_json::Value;
8use std::borrow::Cow;
9
10/// A single patch operation with zero-copy paths where possible.
11#[derive(Debug, Clone)]
12pub enum PatchOperationRef<'a> {
13    /// Add a value at the specified path.
14    Add {
15        /// JSON Pointer path (RFC 6901).
16        path: Cow<'a, str>,
17        /// Value to add.
18        value: Cow<'a, Value>,
19    },
20    /// Remove the value at the specified path.
21    Remove {
22        /// JSON Pointer path.
23        path: Cow<'a, str>,
24    },
25    /// Replace the value at the specified path.
26    Replace {
27        /// JSON Pointer path.
28        path: Cow<'a, str>,
29        /// New value.
30        value: Cow<'a, Value>,
31    },
32    /// Move a value from one path to another.
33    Move {
34        /// Source path.
35        from: Cow<'a, str>,
36        /// Destination path.
37        path: Cow<'a, str>,
38    },
39    /// Copy a value from one path to another.
40    Copy {
41        /// Source path.
42        from: Cow<'a, str>,
43        /// Destination path.
44        path: Cow<'a, str>,
45    },
46    /// Test that a value equals the expected value.
47    Test {
48        /// JSON Pointer path.
49        path: Cow<'a, str>,
50        /// Expected value.
51        value: Cow<'a, Value>,
52    },
53}
54
55impl<'a> PatchOperationRef<'a> {
56    /// Create an Add operation with owned path and borrowed value.
57    #[must_use]
58    pub const fn add(path: String, value: &'a Value) -> Self {
59        Self::Add {
60            path: Cow::Owned(path),
61            value: Cow::Borrowed(value),
62        }
63    }
64
65    /// Create a Remove operation with owned path.
66    #[must_use]
67    pub const fn remove(path: String) -> Self {
68        Self::Remove {
69            path: Cow::Owned(path),
70        }
71    }
72
73    /// Create a Replace operation with owned path and borrowed value.
74    #[must_use]
75    pub const fn replace(path: String, value: &'a Value) -> Self {
76        Self::Replace {
77            path: Cow::Owned(path),
78            value: Cow::Borrowed(value),
79        }
80    }
81
82    /// Create a Move operation with owned paths.
83    #[must_use]
84    pub const fn move_op(from: String, path: String) -> Self {
85        Self::Move {
86            from: Cow::Owned(from),
87            path: Cow::Owned(path),
88        }
89    }
90
91    /// Create a Copy operation with owned paths.
92    #[must_use]
93    pub const fn copy(from: String, path: String) -> Self {
94        Self::Copy {
95            from: Cow::Owned(from),
96            path: Cow::Owned(path),
97        }
98    }
99
100    /// Create a Test operation with owned path and borrowed value.
101    #[must_use]
102    pub const fn test(path: String, value: &'a Value) -> Self {
103        Self::Test {
104            path: Cow::Owned(path),
105            value: Cow::Borrowed(value),
106        }
107    }
108
109    /// Get the operation type as a string.
110    #[must_use]
111    pub const fn op_type(&self) -> &'static str {
112        match self {
113            Self::Add { .. } => "add",
114            Self::Remove { .. } => "remove",
115            Self::Replace { .. } => "replace",
116            Self::Move { .. } => "move",
117            Self::Copy { .. } => "copy",
118            Self::Test { .. } => "test",
119        }
120    }
121
122    /// Get the primary path of this operation.
123    #[must_use]
124    pub fn path(&self) -> &str {
125        match self {
126            Self::Add { path, .. }
127            | Self::Remove { path }
128            | Self::Replace { path, .. }
129            | Self::Move { path, .. }
130            | Self::Copy { path, .. }
131            | Self::Test { path, .. } => path,
132        }
133    }
134
135    /// Convert to owned version (for storing beyond borrow lifetime).
136    #[must_use]
137    pub fn into_owned(self) -> PatchOperationRef<'static> {
138        match self {
139            Self::Add { path, value } => PatchOperationRef::Add {
140                path: Cow::Owned(path.into_owned()),
141                value: Cow::Owned(value.into_owned()),
142            },
143            Self::Remove { path } => PatchOperationRef::Remove {
144                path: Cow::Owned(path.into_owned()),
145            },
146            Self::Replace { path, value } => PatchOperationRef::Replace {
147                path: Cow::Owned(path.into_owned()),
148                value: Cow::Owned(value.into_owned()),
149            },
150            Self::Move { from, path } => PatchOperationRef::Move {
151                from: Cow::Owned(from.into_owned()),
152                path: Cow::Owned(path.into_owned()),
153            },
154            Self::Copy { from, path } => PatchOperationRef::Copy {
155                from: Cow::Owned(from.into_owned()),
156                path: Cow::Owned(path.into_owned()),
157            },
158            Self::Test { path, value } => PatchOperationRef::Test {
159                path: Cow::Owned(path.into_owned()),
160                value: Cow::Owned(value.into_owned()),
161            },
162        }
163    }
164}
165
166/// A JSON Patch document with zero-copy operations.
167#[derive(Debug, Default, Clone)]
168pub struct JsonPatchRef<'a> {
169    /// The operations in this patch.
170    pub operations: Vec<PatchOperationRef<'a>>,
171}
172
173impl<'a> JsonPatchRef<'a> {
174    /// Create a new empty patch.
175    #[must_use]
176    pub const fn new() -> Self {
177        Self {
178            operations: Vec::new(),
179        }
180    }
181
182    /// Create with pre-allocated capacity.
183    #[must_use]
184    pub fn with_capacity(capacity: usize) -> Self {
185        Self {
186            operations: Vec::with_capacity(capacity),
187        }
188    }
189
190    /// Add an operation to the patch.
191    pub fn push(&mut self, op: PatchOperationRef<'a>) {
192        self.operations.push(op);
193    }
194
195    /// Get the number of operations.
196    #[must_use]
197    pub const fn len(&self) -> usize {
198        self.operations.len()
199    }
200
201    /// Check if patch is empty.
202    #[must_use]
203    pub const fn is_empty(&self) -> bool {
204        self.operations.is_empty()
205    }
206
207    /// Iterate over operations.
208    pub fn iter(&self) -> impl Iterator<Item = &PatchOperationRef<'a>> {
209        self.operations.iter()
210    }
211
212    /// Convert to owned version.
213    #[must_use]
214    pub fn into_owned(self) -> JsonPatchRef<'static> {
215        JsonPatchRef {
216            operations: self
217                .operations
218                .into_iter()
219                .map(PatchOperationRef::into_owned)
220                .collect(),
221        }
222    }
223
224    /// Convert to the standard (allocating) `JsonPatch` format.
225    #[must_use]
226    pub fn to_json_patch(&self) -> super::patch::JsonPatch {
227        use super::patch::PatchOperation;
228
229        let ops: Vec<PatchOperation> = self
230            .operations
231            .iter()
232            .map(|op| match op {
233                PatchOperationRef::Add { path, value } => PatchOperation::Add {
234                    path: path.to_string(),
235                    value: value.as_ref().clone(),
236                },
237                PatchOperationRef::Remove { path } => PatchOperation::Remove {
238                    path: path.to_string(),
239                },
240                PatchOperationRef::Replace { path, value } => PatchOperation::Replace {
241                    path: path.to_string(),
242                    value: value.as_ref().clone(),
243                },
244                PatchOperationRef::Move { from, path } => PatchOperation::Move {
245                    from: from.to_string(),
246                    path: path.to_string(),
247                },
248                PatchOperationRef::Copy { from, path } => PatchOperation::Copy {
249                    from: from.to_string(),
250                    path: path.to_string(),
251                },
252                PatchOperationRef::Test { path, value } => PatchOperation::Test {
253                    path: path.to_string(),
254                    value: value.as_ref().clone(),
255                },
256            })
257            .collect();
258
259        super::patch::JsonPatch { operations: ops }
260    }
261}
262
263impl<'a> IntoIterator for JsonPatchRef<'a> {
264    type Item = PatchOperationRef<'a>;
265    type IntoIter = std::vec::IntoIter<PatchOperationRef<'a>>;
266
267    fn into_iter(self) -> Self::IntoIter {
268        self.operations.into_iter()
269    }
270}
271
272impl<'a, 'b> IntoIterator for &'b JsonPatchRef<'a> {
273    type Item = &'b PatchOperationRef<'a>;
274    type IntoIter = std::slice::Iter<'b, PatchOperationRef<'a>>;
275
276    fn into_iter(self) -> Self::IntoIter {
277        self.operations.iter()
278    }
279}
280
281/// Generate a zero-copy JSON Patch that transforms `source` into `target`.
282///
283/// # Arguments
284///
285/// * `source` - The original JSON document
286/// * `target` - The desired JSON document
287///
288/// # Returns
289///
290/// A `JsonPatchRef` containing operations to transform source into target.
291/// The operations borrow values from `target` where possible.
292#[must_use]
293pub fn json_diff_zerocopy<'a>(source: &Value, target: &'a Value) -> JsonPatchRef<'a> {
294    let mut patch = JsonPatchRef::with_capacity(8);
295    diff_values_zerocopy(source, target, String::new(), &mut patch);
296    patch
297}
298
299/// Recursively diff two values with zero-copy output.
300#[allow(clippy::similar_names)]
301fn diff_values_zerocopy<'a>(
302    source: &Value,
303    target: &'a Value,
304    path: String,
305    patch: &mut JsonPatchRef<'a>,
306) {
307    // Fast path: identical values
308    if source == target {
309        return;
310    }
311
312    match (source, target) {
313        // Both objects - compare fields
314        (Value::Object(src_map), Value::Object(tgt_map)) => {
315            // Find removed keys
316            for key in src_map.keys() {
317                if !tgt_map.contains_key(key) {
318                    let key_path = if path.is_empty() {
319                        format!("/{}", escape_json_pointer(key))
320                    } else {
321                        format!("{}/{}", path, escape_json_pointer(key))
322                    };
323                    patch.push(PatchOperationRef::remove(key_path));
324                }
325            }
326
327            // Find added/changed keys
328            for (key, tgt_val) in tgt_map {
329                let key_path = if path.is_empty() {
330                    format!("/{}", escape_json_pointer(key))
331                } else {
332                    format!("{}/{}", path, escape_json_pointer(key))
333                };
334
335                match src_map.get(key) {
336                    Some(src_val) => {
337                        // Key exists - recurse
338                        diff_values_zerocopy(src_val, tgt_val, key_path, patch);
339                    }
340                    None => {
341                        // Key added
342                        patch.push(PatchOperationRef::add(key_path, tgt_val));
343                    }
344                }
345            }
346        }
347
348        // Both arrays - compare elements
349        (Value::Array(src_arr), Value::Array(tgt_arr)) => {
350            let src_len = src_arr.len();
351            let tgt_len = tgt_arr.len();
352
353            // Compare common prefix
354            let min_len = src_len.min(tgt_len);
355            for i in 0..min_len {
356                let idx_path = if path.is_empty() {
357                    format!("/{i}")
358                } else {
359                    format!("{path}/{i}")
360                };
361                diff_values_zerocopy(&src_arr[i], &tgt_arr[i], idx_path, patch);
362            }
363
364            // Handle length differences
365            if tgt_len > src_len {
366                // Add new elements
367                for (i, item) in tgt_arr.iter().enumerate().take(tgt_len).skip(src_len) {
368                    let idx_path = if path.is_empty() {
369                        format!("/{i}")
370                    } else {
371                        format!("{path}/{i}")
372                    };
373                    patch.push(PatchOperationRef::add(idx_path, item));
374                }
375            } else if src_len > tgt_len {
376                // Remove extra elements (in reverse order for valid indexing)
377                for i in (tgt_len..src_len).rev() {
378                    let idx_path = if path.is_empty() {
379                        format!("/{i}")
380                    } else {
381                        format!("{path}/{i}")
382                    };
383                    patch.push(PatchOperationRef::remove(idx_path));
384                }
385            }
386        }
387
388        // Different types or different values - replace
389        _ => {
390            if path.is_empty() {
391                // Root replacement - use empty path per RFC 6902
392                patch.push(PatchOperationRef::replace(String::new(), target));
393            } else {
394                patch.push(PatchOperationRef::replace(path, target));
395            }
396        }
397    }
398}
399
400/// Escape a string for use in a JSON Pointer (RFC 6901).
401fn escape_json_pointer(s: &str) -> Cow<'_, str> {
402    if s.contains('~') || s.contains('/') {
403        let escaped = s.replace('~', "~0").replace('/', "~1");
404        Cow::Owned(escaped)
405    } else {
406        Cow::Borrowed(s)
407    }
408}
409
410#[cfg(test)]
411mod tests {
412    use super::*;
413    use serde_json::json;
414
415    // =========================================================================
416    // PatchOperationRef Constructor Tests
417    // =========================================================================
418
419    #[test]
420    fn test_patch_operation_add() {
421        let value = json!(42);
422        let op = PatchOperationRef::add("/path".to_string(), &value);
423        assert_eq!(op.op_type(), "add");
424        assert_eq!(op.path(), "/path");
425    }
426
427    #[test]
428    fn test_patch_operation_remove() {
429        let op = PatchOperationRef::remove("/path".to_string());
430        assert_eq!(op.op_type(), "remove");
431        assert_eq!(op.path(), "/path");
432    }
433
434    #[test]
435    fn test_patch_operation_replace() {
436        let value = json!("new");
437        let op = PatchOperationRef::replace("/path".to_string(), &value);
438        assert_eq!(op.op_type(), "replace");
439        assert_eq!(op.path(), "/path");
440    }
441
442    #[test]
443    fn test_patch_operation_move() {
444        let op = PatchOperationRef::move_op("/from".to_string(), "/to".to_string());
445        assert_eq!(op.op_type(), "move");
446        assert_eq!(op.path(), "/to");
447    }
448
449    #[test]
450    fn test_patch_operation_copy() {
451        let op = PatchOperationRef::copy("/from".to_string(), "/to".to_string());
452        assert_eq!(op.op_type(), "copy");
453        assert_eq!(op.path(), "/to");
454    }
455
456    #[test]
457    fn test_patch_operation_test() {
458        let value = json!(true);
459        let op = PatchOperationRef::test("/path".to_string(), &value);
460        assert_eq!(op.op_type(), "test");
461        assert_eq!(op.path(), "/path");
462    }
463
464    // =========================================================================
465    // PatchOperationRef into_owned Tests
466    // =========================================================================
467
468    #[test]
469    fn test_patch_operation_into_owned_add() {
470        let value = json!(42);
471        let op = PatchOperationRef::add("/path".to_string(), &value);
472        let owned = op.into_owned();
473        assert_eq!(owned.op_type(), "add");
474        assert_eq!(owned.path(), "/path");
475    }
476
477    #[test]
478    fn test_patch_operation_into_owned_remove() {
479        let op = PatchOperationRef::remove("/path".to_string());
480        let owned = op.into_owned();
481        assert_eq!(owned.op_type(), "remove");
482        assert_eq!(owned.path(), "/path");
483    }
484
485    #[test]
486    fn test_patch_operation_into_owned_replace() {
487        let value = json!("new");
488        let op = PatchOperationRef::replace("/path".to_string(), &value);
489        let owned = op.into_owned();
490        assert_eq!(owned.op_type(), "replace");
491    }
492
493    #[test]
494    fn test_patch_operation_into_owned_move() {
495        let op = PatchOperationRef::move_op("/from".to_string(), "/to".to_string());
496        let owned = op.into_owned();
497        assert_eq!(owned.op_type(), "move");
498    }
499
500    #[test]
501    fn test_patch_operation_into_owned_copy() {
502        let op = PatchOperationRef::copy("/from".to_string(), "/to".to_string());
503        let owned = op.into_owned();
504        assert_eq!(owned.op_type(), "copy");
505    }
506
507    #[test]
508    fn test_patch_operation_into_owned_test() {
509        let value = json!(true);
510        let op = PatchOperationRef::test("/path".to_string(), &value);
511        let owned = op.into_owned();
512        assert_eq!(owned.op_type(), "test");
513    }
514
515    #[test]
516    fn test_patch_operation_debug() {
517        let value = json!(42);
518        let op = PatchOperationRef::add("/path".to_string(), &value);
519        let debug_str = format!("{op:?}");
520        assert!(debug_str.contains("Add"));
521    }
522
523    #[test]
524    fn test_patch_operation_clone() {
525        let value = json!(42);
526        let op = PatchOperationRef::add("/path".to_string(), &value);
527        let cloned = op.clone();
528        assert_eq!(cloned.path(), "/path");
529    }
530
531    // =========================================================================
532    // JsonPatchRef Tests
533    // =========================================================================
534
535    #[test]
536    fn test_json_patch_ref_new() {
537        let patch = JsonPatchRef::new();
538        assert!(patch.is_empty());
539        assert_eq!(patch.len(), 0);
540    }
541
542    #[test]
543    fn test_json_patch_ref_with_capacity() {
544        let patch = JsonPatchRef::with_capacity(10);
545        assert!(patch.is_empty());
546    }
547
548    #[test]
549    fn test_json_patch_ref_push() {
550        let value = json!(42);
551        let mut patch = JsonPatchRef::new();
552        patch.push(PatchOperationRef::add("/a".to_string(), &value));
553        assert_eq!(patch.len(), 1);
554    }
555
556    #[test]
557    fn test_json_patch_ref_iter() {
558        let value = json!(42);
559        let mut patch = JsonPatchRef::new();
560        patch.push(PatchOperationRef::add("/a".to_string(), &value));
561        patch.push(PatchOperationRef::remove("/b".to_string()));
562
563        let paths: Vec<_> = patch.iter().map(super::PatchOperationRef::path).collect();
564        assert_eq!(paths.len(), 2);
565        assert_eq!(paths[0], "/a");
566        assert_eq!(paths[1], "/b");
567    }
568
569    #[test]
570    fn test_json_patch_ref_into_iter_owned() {
571        let value = json!(42);
572        let mut patch = JsonPatchRef::new();
573        patch.push(PatchOperationRef::add("/a".to_string(), &value));
574
575        let ops_count = patch.into_iter().count();
576        assert_eq!(ops_count, 1);
577    }
578
579    #[test]
580    fn test_json_patch_ref_into_iter_ref() {
581        let value = json!(42);
582        let mut patch = JsonPatchRef::new();
583        patch.push(PatchOperationRef::add("/a".to_string(), &value));
584
585        let ops_count = (&patch).into_iter().count();
586        assert_eq!(ops_count, 1);
587    }
588
589    #[test]
590    fn test_json_patch_ref_default() {
591        let patch = JsonPatchRef::default();
592        assert!(patch.is_empty());
593    }
594
595    #[test]
596    fn test_json_patch_ref_debug() {
597        let patch = JsonPatchRef::new();
598        let debug_str = format!("{patch:?}");
599        assert!(debug_str.contains("JsonPatchRef"));
600    }
601
602    #[test]
603    fn test_json_patch_ref_clone() {
604        let value = json!(42);
605        let mut patch = JsonPatchRef::new();
606        patch.push(PatchOperationRef::add("/a".to_string(), &value));
607        let cloned = patch.clone();
608        assert_eq!(cloned.len(), 1);
609    }
610
611    // =========================================================================
612    // to_json_patch Conversion Tests
613    // =========================================================================
614
615    #[test]
616    fn test_to_json_patch_add() {
617        let value = json!(42);
618        let mut patch = JsonPatchRef::new();
619        patch.push(PatchOperationRef::add("/a".to_string(), &value));
620        let json_patch = patch.to_json_patch();
621        assert_eq!(json_patch.operations.len(), 1);
622    }
623
624    #[test]
625    fn test_to_json_patch_remove() {
626        let mut patch = JsonPatchRef::new();
627        patch.push(PatchOperationRef::remove("/a".to_string()));
628        let json_patch = patch.to_json_patch();
629        assert_eq!(json_patch.operations.len(), 1);
630    }
631
632    #[test]
633    fn test_to_json_patch_replace() {
634        let value = json!("new");
635        let mut patch = JsonPatchRef::new();
636        patch.push(PatchOperationRef::replace("/a".to_string(), &value));
637        let json_patch = patch.to_json_patch();
638        assert_eq!(json_patch.operations.len(), 1);
639    }
640
641    #[test]
642    fn test_to_json_patch_move() {
643        let mut patch = JsonPatchRef::new();
644        patch.push(PatchOperationRef::move_op(
645            "/from".to_string(),
646            "/to".to_string(),
647        ));
648        let json_patch = patch.to_json_patch();
649        assert_eq!(json_patch.operations.len(), 1);
650    }
651
652    #[test]
653    fn test_to_json_patch_copy() {
654        let mut patch = JsonPatchRef::new();
655        patch.push(PatchOperationRef::copy(
656            "/from".to_string(),
657            "/to".to_string(),
658        ));
659        let json_patch = patch.to_json_patch();
660        assert_eq!(json_patch.operations.len(), 1);
661    }
662
663    #[test]
664    fn test_to_json_patch_test() {
665        let value = json!(true);
666        let mut patch = JsonPatchRef::new();
667        patch.push(PatchOperationRef::test("/a".to_string(), &value));
668        let json_patch = patch.to_json_patch();
669        assert_eq!(json_patch.operations.len(), 1);
670    }
671
672    // =========================================================================
673    // json_diff_zerocopy Tests
674    // =========================================================================
675
676    #[test]
677    fn test_empty_diff() {
678        let doc = json!({"a": 1});
679        let patch = json_diff_zerocopy(&doc, &doc);
680        assert!(patch.is_empty());
681    }
682
683    #[test]
684    fn test_add_field() {
685        let source = json!({});
686        let target = json!({"name": "Alice"});
687        let patch = json_diff_zerocopy(&source, &target);
688
689        assert_eq!(patch.len(), 1);
690        assert_eq!(patch.operations[0].op_type(), "add");
691        assert_eq!(patch.operations[0].path(), "/name");
692    }
693
694    #[test]
695    fn test_remove_field() {
696        let source = json!({"name": "Alice"});
697        let target = json!({});
698        let patch = json_diff_zerocopy(&source, &target);
699
700        assert_eq!(patch.len(), 1);
701        assert_eq!(patch.operations[0].op_type(), "remove");
702    }
703
704    #[test]
705    fn test_replace_value() {
706        let source = json!({"name": "Alice"});
707        let target = json!({"name": "Bob"});
708        let patch = json_diff_zerocopy(&source, &target);
709
710        assert_eq!(patch.len(), 1);
711        assert_eq!(patch.operations[0].op_type(), "replace");
712    }
713
714    #[test]
715    fn test_nested_diff() {
716        let source = json!({"user": {"name": "Alice"}});
717        let target = json!({"user": {"name": "Bob"}});
718        let patch = json_diff_zerocopy(&source, &target);
719
720        assert_eq!(patch.len(), 1);
721        assert_eq!(patch.operations[0].path(), "/user/name");
722    }
723
724    #[test]
725    fn test_array_diff_add() {
726        let source = json!([1, 2, 3]);
727        let target = json!([1, 2, 3, 4]);
728        let patch = json_diff_zerocopy(&source, &target);
729
730        assert_eq!(patch.len(), 1);
731        assert_eq!(patch.operations[0].op_type(), "add");
732    }
733
734    #[test]
735    fn test_array_diff_remove() {
736        let source = json!([1, 2, 3, 4]);
737        let target = json!([1, 2]);
738        let patch = json_diff_zerocopy(&source, &target);
739
740        // Should remove indices 3 and 2 in reverse order
741        assert_eq!(patch.len(), 2);
742        assert_eq!(patch.operations[0].op_type(), "remove");
743        assert_eq!(patch.operations[1].op_type(), "remove");
744    }
745
746    #[test]
747    fn test_array_diff_change() {
748        let source = json!([1, 2, 3]);
749        let target = json!([1, 99, 3]);
750        let patch = json_diff_zerocopy(&source, &target);
751
752        assert_eq!(patch.len(), 1);
753        assert_eq!(patch.operations[0].path(), "/1");
754        assert_eq!(patch.operations[0].op_type(), "replace");
755    }
756
757    #[test]
758    fn test_type_change() {
759        let source = json!({"x": 1});
760        let target = json!({"x": "one"});
761        let patch = json_diff_zerocopy(&source, &target);
762
763        assert_eq!(patch.len(), 1);
764        assert_eq!(patch.operations[0].op_type(), "replace");
765    }
766
767    #[test]
768    fn test_root_replacement() {
769        let source = json!(42);
770        let target = json!("hello");
771        let patch = json_diff_zerocopy(&source, &target);
772
773        assert_eq!(patch.len(), 1);
774        assert_eq!(patch.operations[0].op_type(), "replace");
775        assert_eq!(patch.operations[0].path(), ""); // Empty path for root
776    }
777
778    #[test]
779    fn test_nested_array_in_object() {
780        let source = json!({"items": [1, 2]});
781        let target = json!({"items": [1, 2, 3]});
782        let patch = json_diff_zerocopy(&source, &target);
783
784        assert_eq!(patch.len(), 1);
785        assert_eq!(patch.operations[0].path(), "/items/2");
786    }
787
788    #[test]
789    fn test_into_owned() {
790        let source = json!({});
791        let target = json!({"x": 1});
792        let patch = json_diff_zerocopy(&source, &target);
793        let owned: JsonPatchRef<'static> = patch.into_owned();
794
795        assert!(!owned.is_empty());
796    }
797
798    #[test]
799    fn test_to_json_patch() {
800        let source = json!({});
801        let target = json!({"x": 1});
802        let patch_ref = json_diff_zerocopy(&source, &target);
803        let patch = patch_ref.to_json_patch();
804
805        assert_eq!(patch.operations.len(), 1);
806    }
807
808    #[test]
809    fn test_escape_json_pointer() {
810        assert_eq!(escape_json_pointer("simple"), "simple");
811        assert_eq!(escape_json_pointer("has/slash"), "has~1slash");
812        assert_eq!(escape_json_pointer("has~tilde"), "has~0tilde");
813        assert_eq!(escape_json_pointer("both~and/"), "both~0and~1");
814    }
815
816    #[test]
817    fn test_diff_with_escaped_keys() {
818        let source = json!({});
819        let target = json!({"a/b": 1, "c~d": 2});
820        let patch = json_diff_zerocopy(&source, &target);
821
822        assert_eq!(patch.len(), 2);
823        // Paths should have escaped keys
824        let paths: Vec<_> = patch.iter().map(super::PatchOperationRef::path).collect();
825        assert!(paths.contains(&"/a~1b") || paths.contains(&"/c~0d"));
826    }
827
828    #[test]
829    fn test_deeply_nested_diff() {
830        let source = json!({"a": {"b": {"c": {"d": 1}}}});
831        let target = json!({"a": {"b": {"c": {"d": 2}}}});
832        let patch = json_diff_zerocopy(&source, &target);
833
834        assert_eq!(patch.len(), 1);
835        assert_eq!(patch.operations[0].path(), "/a/b/c/d");
836    }
837
838    #[test]
839    fn test_empty_array_to_non_empty() {
840        let source = json!([]);
841        let target = json!([1, 2, 3]);
842        let patch = json_diff_zerocopy(&source, &target);
843
844        assert_eq!(patch.len(), 3);
845    }
846
847    #[test]
848    fn test_non_empty_array_to_empty() {
849        let source = json!([1, 2, 3]);
850        let target = json!([]);
851        let patch = json_diff_zerocopy(&source, &target);
852
853        assert_eq!(patch.len(), 3);
854        // All should be remove operations
855        for op in patch.iter() {
856            assert_eq!(op.op_type(), "remove");
857        }
858    }
859
860    #[test]
861    fn test_object_to_array_type_change() {
862        let source = json!({"a": 1});
863        let target = json!([1]);
864        let patch = json_diff_zerocopy(&source, &target);
865
866        assert_eq!(patch.len(), 1);
867        assert_eq!(patch.operations[0].op_type(), "replace");
868        assert_eq!(patch.operations[0].path(), "");
869    }
870
871    #[test]
872    fn test_nested_object_add() {
873        let source = json!({"user": {}});
874        let target = json!({"user": {"name": "Alice"}});
875        let patch = json_diff_zerocopy(&source, &target);
876
877        assert_eq!(patch.len(), 1);
878        assert_eq!(patch.operations[0].op_type(), "add");
879        assert_eq!(patch.operations[0].path(), "/user/name");
880    }
881
882    #[test]
883    fn test_nested_object_remove() {
884        let source = json!({"user": {"name": "Alice", "age": 30}});
885        let target = json!({"user": {"name": "Alice"}});
886        let patch = json_diff_zerocopy(&source, &target);
887
888        assert_eq!(patch.len(), 1);
889        assert_eq!(patch.operations[0].op_type(), "remove");
890        assert_eq!(patch.operations[0].path(), "/user/age");
891    }
892}