1use std::fmt;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub enum DiffValueKind {
39 Null,
41 Bool,
43 Number,
45 String,
47 Array,
49 Object,
51}
52
53impl DiffValueKind {
54 #[must_use]
56 pub const fn is_scalar(self) -> bool {
57 matches!(self, Self::Null | Self::Bool | Self::Number | Self::String)
58 }
59
60 #[must_use]
62 pub const fn is_container(self) -> bool {
63 matches!(self, Self::Array | Self::Object)
64 }
65}
66
67impl fmt::Display for DiffValueKind {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 match self {
70 Self::Null => write!(f, "null"),
71 Self::Bool => write!(f, "boolean"),
72 Self::Number => write!(f, "number"),
73 Self::String => write!(f, "string"),
74 Self::Array => write!(f, "array"),
75 Self::Object => write!(f, "object"),
76 }
77 }
78}
79
80pub trait DiffableValue: Clone + PartialEq {
97 fn equals(&self, other: &Self) -> bool;
99
100 fn value_kind(&self) -> DiffValueKind;
102
103 fn as_object(&self) -> Option<Box<dyn Iterator<Item = (&str, &Self)> + '_>>;
107
108 fn object_keys(&self) -> Option<Vec<&str>> {
110 self.as_object().map(|iter| iter.map(|(k, _)| k).collect())
111 }
112
113 fn get_field(&self, key: &str) -> Option<&Self>;
115
116 fn as_array(&self) -> Option<&[Self]>
118 where
119 Self: Sized;
120
121 fn array_len(&self) -> Option<usize> {
123 self.as_array().map(<[Self]>::len)
124 }
125
126 fn get_element(&self, index: usize) -> Option<&Self>
128 where
129 Self: Sized,
130 {
131 self.as_array().and_then(|arr| arr.get(index))
132 }
133
134 fn as_str(&self) -> Option<&str>;
136
137 fn as_bool(&self) -> Option<bool>;
139
140 fn as_i64(&self) -> Option<i64>;
142
143 fn as_f64(&self) -> Option<f64>;
145
146 #[must_use]
148 fn deep_clone(&self) -> Self
149 where
150 Self: Sized,
151 {
152 self.clone()
153 }
154
155 fn is_null(&self) -> bool {
157 matches!(self.value_kind(), DiffValueKind::Null)
158 }
159
160 fn is_object(&self) -> bool {
162 matches!(self.value_kind(), DiffValueKind::Object)
163 }
164
165 fn is_array(&self) -> bool {
167 matches!(self.value_kind(), DiffValueKind::Array)
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
179pub enum GenericPatchOperation<V: DiffableValue> {
180 Add {
182 path: String,
184 value: V,
186 },
187 Remove {
189 path: String,
191 },
192 Replace {
194 path: String,
196 value: V,
198 },
199 Move {
201 from: String,
203 path: String,
205 },
206 Copy {
208 from: String,
210 path: String,
212 },
213 Test {
215 path: String,
217 value: V,
219 },
220}
221
222impl<V: DiffableValue> GenericPatchOperation<V> {
223 #[must_use]
225 pub fn path(&self) -> &str {
226 match self {
227 Self::Add { path, .. }
228 | Self::Remove { path }
229 | Self::Replace { path, .. }
230 | Self::Move { path, .. }
231 | Self::Copy { path, .. }
232 | Self::Test { path, .. } => path,
233 }
234 }
235
236 #[must_use]
238 pub fn from_path(&self) -> Option<&str> {
239 match self {
240 Self::Move { from, .. } | Self::Copy { from, .. } => Some(from),
241 _ => None,
242 }
243 }
244
245 #[must_use]
247 pub const fn is_add(&self) -> bool {
248 matches!(self, Self::Add { .. })
249 }
250
251 #[must_use]
253 pub const fn is_remove(&self) -> bool {
254 matches!(self, Self::Remove { .. })
255 }
256
257 #[must_use]
259 pub const fn is_replace(&self) -> bool {
260 matches!(self, Self::Replace { .. })
261 }
262}
263
264impl<V: DiffableValue + fmt::Debug> fmt::Display for GenericPatchOperation<V> {
265 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266 match self {
267 Self::Add { path, value } => write!(f, "add({path}, {value:?})"),
268 Self::Remove { path } => write!(f, "remove({path})"),
269 Self::Replace { path, value } => write!(f, "replace({path}, {value:?})"),
270 Self::Move { from, path } => write!(f, "move({from} -> {path})"),
271 Self::Copy { from, path } => write!(f, "copy({from} -> {path})"),
272 Self::Test { path, value } => write!(f, "test({path}, {value:?})"),
273 }
274 }
275}
276
277#[derive(Debug, Clone, Default)]
285pub struct GenericPatch<V: DiffableValue> {
286 pub operations: Vec<GenericPatchOperation<V>>,
288}
289
290impl<V: DiffableValue> GenericPatch<V> {
291 #[must_use]
293 pub const fn new() -> Self {
294 Self {
295 operations: Vec::new(),
296 }
297 }
298
299 #[must_use]
301 pub const fn with_operations(operations: Vec<GenericPatchOperation<V>>) -> Self {
302 Self { operations }
303 }
304
305 pub fn push(&mut self, op: GenericPatchOperation<V>) {
307 self.operations.push(op);
308 }
309
310 #[must_use]
312 pub const fn is_empty(&self) -> bool {
313 self.operations.is_empty()
314 }
315
316 #[must_use]
318 pub const fn len(&self) -> usize {
319 self.operations.len()
320 }
321
322 pub fn iter(&self) -> impl Iterator<Item = &GenericPatchOperation<V>> {
324 self.operations.iter()
325 }
326
327 #[must_use]
329 pub fn operations_at_prefix(&self, prefix: &str) -> Vec<&GenericPatchOperation<V>> {
330 self.operations
331 .iter()
332 .filter(|op| op.path().starts_with(prefix))
333 .collect()
334 }
335}
336
337impl<V: DiffableValue> FromIterator<GenericPatchOperation<V>> for GenericPatch<V> {
338 fn from_iter<I: IntoIterator<Item = GenericPatchOperation<V>>>(iter: I) -> Self {
339 Self {
340 operations: iter.into_iter().collect(),
341 }
342 }
343}
344
345impl<V: DiffableValue> IntoIterator for GenericPatch<V> {
346 type Item = GenericPatchOperation<V>;
347 type IntoIter = std::vec::IntoIter<GenericPatchOperation<V>>;
348
349 fn into_iter(self) -> Self::IntoIter {
350 self.operations.into_iter()
351 }
352}
353
354impl DiffableValue for serde_json::Value {
359 fn equals(&self, other: &Self) -> bool {
360 self == other
361 }
362
363 fn value_kind(&self) -> DiffValueKind {
364 match self {
365 Self::Null => DiffValueKind::Null,
366 Self::Bool(_) => DiffValueKind::Bool,
367 Self::Number(_) => DiffValueKind::Number,
368 Self::String(_) => DiffValueKind::String,
369 Self::Array(_) => DiffValueKind::Array,
370 Self::Object(_) => DiffValueKind::Object,
371 }
372 }
373
374 fn as_object(&self) -> Option<Box<dyn Iterator<Item = (&str, &Self)> + '_>> {
375 match self {
376 Self::Object(map) => Some(Box::new(map.iter().map(|(k, v)| (k.as_str(), v)))),
377 _ => None,
378 }
379 }
380
381 fn get_field(&self, key: &str) -> Option<&Self> {
382 match self {
383 Self::Object(map) => map.get(key),
384 _ => None,
385 }
386 }
387
388 fn as_array(&self) -> Option<&[Self]> {
389 match self {
390 Self::Array(arr) => Some(arr.as_slice()),
391 _ => None,
392 }
393 }
394
395 fn as_str(&self) -> Option<&str> {
396 self.as_str()
397 }
398
399 fn as_bool(&self) -> Option<bool> {
400 self.as_bool()
401 }
402
403 fn as_i64(&self) -> Option<i64> {
404 self.as_i64()
405 }
406
407 fn as_f64(&self) -> Option<f64> {
408 self.as_f64()
409 }
410}
411
412#[derive(Debug, Clone, Default)]
418pub struct DiffOptions {
419 pub detect_reorder: bool,
421 pub detect_moves: bool,
423 pub max_depth: usize,
425}
426
427impl DiffOptions {
428 #[must_use]
430 pub fn new() -> Self {
431 Self::default()
432 }
433
434 #[must_use]
436 pub const fn with_move_detection(mut self) -> Self {
437 self.detect_moves = true;
438 self
439 }
440
441 #[must_use]
443 pub const fn with_reorder_detection(mut self) -> Self {
444 self.detect_reorder = true;
445 self
446 }
447
448 #[must_use]
450 pub const fn with_max_depth(mut self, depth: usize) -> Self {
451 self.max_depth = depth;
452 self
453 }
454}
455
456#[must_use]
460pub fn compute_diff<V: DiffableValue>(source: &V, target: &V) -> GenericPatch<V> {
461 compute_diff_with_options(source, target, &DiffOptions::default())
462}
463
464#[must_use]
466pub fn compute_diff_with_options<V: DiffableValue>(
467 source: &V,
468 target: &V,
469 options: &DiffOptions,
470) -> GenericPatch<V> {
471 let mut patch = GenericPatch::new();
472 diff_values(source, target, "", &mut patch, options, 0);
473 patch
474}
475
476fn diff_values<V: DiffableValue>(
477 source: &V,
478 target: &V,
479 current_path: &str,
480 result: &mut GenericPatch<V>,
481 options: &DiffOptions,
482 depth: usize,
483) {
484 if options.max_depth > 0 && depth >= options.max_depth {
486 if !source.equals(target) {
487 result.push(GenericPatchOperation::Replace {
488 path: current_path.to_string(),
489 value: target.deep_clone(),
490 });
491 }
492 return;
493 }
494
495 if source.equals(target) {
497 return;
498 }
499
500 if source.value_kind() != target.value_kind() {
502 result.push(GenericPatchOperation::Replace {
503 path: current_path.to_string(),
504 value: target.deep_clone(),
505 });
506 return;
507 }
508
509 match (source.value_kind(), target.value_kind()) {
511 (DiffValueKind::Object, DiffValueKind::Object) => {
512 diff_objects(source, target, current_path, result, options, depth);
513 }
514 (DiffValueKind::Array, DiffValueKind::Array) => {
515 diff_arrays(source, target, current_path, result, options, depth);
516 }
517 _ => {
518 result.push(GenericPatchOperation::Replace {
520 path: current_path.to_string(),
521 value: target.deep_clone(),
522 });
523 }
524 }
525}
526
527fn diff_objects<V: DiffableValue>(
528 source: &V,
529 target: &V,
530 current_path: &str,
531 result: &mut GenericPatch<V>,
532 options: &DiffOptions,
533 depth: usize,
534) {
535 let src_keys: std::collections::HashSet<_> = source
536 .object_keys()
537 .map(|keys| keys.into_iter().collect())
538 .unwrap_or_default();
539 let tgt_keys: std::collections::HashSet<_> = target
540 .object_keys()
541 .map(|keys| keys.into_iter().collect())
542 .unwrap_or_default();
543
544 for key in &src_keys {
546 if !tgt_keys.contains(key) {
547 let field_path = format_path(current_path, key);
548 result.push(GenericPatchOperation::Remove { path: field_path });
549 }
550 }
551
552 for key in &tgt_keys {
554 let field_path = format_path(current_path, key);
555
556 if src_keys.contains(key) {
557 if let (Some(src_val), Some(tgt_val)) = (source.get_field(key), target.get_field(key)) {
559 diff_values(src_val, tgt_val, &field_path, result, options, depth + 1);
560 }
561 } else {
562 if let Some(val) = target.get_field(key) {
564 result.push(GenericPatchOperation::Add {
565 path: field_path,
566 value: val.deep_clone(),
567 });
568 }
569 }
570 }
571}
572
573fn diff_arrays<V: DiffableValue>(
574 source: &V,
575 target: &V,
576 current_path: &str,
577 result: &mut GenericPatch<V>,
578 options: &DiffOptions,
579 depth: usize,
580) {
581 let src_arr = source.as_array().unwrap_or(&[]);
582 let tgt_arr = target.as_array().unwrap_or(&[]);
583
584 let src_len = src_arr.len();
585 let tgt_len = tgt_arr.len();
586
587 let min_len = src_len.min(tgt_len);
589
590 for (idx, (src_elem, tgt_elem)) in src_arr.iter().zip(tgt_arr.iter()).take(min_len).enumerate()
592 {
593 let elem_path = format!("{current_path}/{idx}");
594 diff_values(src_elem, tgt_elem, &elem_path, result, options, depth + 1);
595 }
596
597 if tgt_len > src_len {
599 for (idx, elem) in tgt_arr.iter().enumerate().skip(src_len) {
601 let elem_path = format!("{current_path}/{idx}");
602 result.push(GenericPatchOperation::Add {
603 path: elem_path,
604 value: elem.deep_clone(),
605 });
606 }
607 } else if src_len > tgt_len {
608 for idx in (tgt_len..src_len).rev() {
610 let elem_path = format!("{current_path}/{idx}");
611 result.push(GenericPatchOperation::Remove { path: elem_path });
612 }
613 }
614}
615
616fn format_path(base: &str, key: &str) -> String {
617 if base.is_empty() {
618 format!("/{}", escape_json_pointer(key))
619 } else {
620 format!("{base}/{}", escape_json_pointer(key))
621 }
622}
623
624fn escape_json_pointer(s: &str) -> String {
625 s.replace('~', "~0").replace('/', "~1")
626}
627
628#[cfg(test)]
633mod tests {
634 use super::*;
635 use serde_json::json;
636
637 #[test]
638 fn test_diff_value_kind() {
639 assert!(DiffValueKind::Null.is_scalar());
640 assert!(DiffValueKind::Bool.is_scalar());
641 assert!(DiffValueKind::Number.is_scalar());
642 assert!(DiffValueKind::String.is_scalar());
643 assert!(!DiffValueKind::Array.is_scalar());
644 assert!(!DiffValueKind::Object.is_scalar());
645
646 assert!(DiffValueKind::Array.is_container());
647 assert!(DiffValueKind::Object.is_container());
648 }
649
650 #[test]
651 fn test_diffable_value_json() {
652 let val = json!({"name": "Alice", "age": 30});
653
654 assert_eq!(val.value_kind(), DiffValueKind::Object);
655 assert!(val.is_object());
656
657 let keys = val.object_keys().unwrap();
658 assert!(keys.contains(&"name"));
659 assert!(keys.contains(&"age"));
660
661 assert_eq!(val.get_field("name"), Some(&json!("Alice")));
662 }
663
664 #[test]
665 fn test_diffable_value_array() {
666 let val = json!([1, 2, 3]);
667
668 assert_eq!(val.value_kind(), DiffValueKind::Array);
669 assert!(val.is_array());
670 assert_eq!(val.array_len(), Some(3));
671 assert_eq!(val.get_element(0), Some(&json!(1)));
672 }
673
674 #[test]
675 fn test_compute_diff_equal() {
676 let source = json!({"name": "Alice"});
677 let target = json!({"name": "Alice"});
678
679 let patch = compute_diff(&source, &target);
680 assert!(patch.is_empty());
681 }
682
683 #[test]
684 fn test_compute_diff_replace_scalar() {
685 let source = json!({"name": "Alice"});
686 let target = json!({"name": "Bob"});
687
688 let patch = compute_diff(&source, &target);
689 assert_eq!(patch.len(), 1);
690 assert!(
691 matches!(&patch.operations[0], GenericPatchOperation::Replace { path, .. } if path == "/name")
692 );
693 }
694
695 #[test]
696 fn test_compute_diff_add_field() {
697 let source = json!({"name": "Alice"});
698 let target = json!({"name": "Alice", "age": 30});
699
700 let patch = compute_diff(&source, &target);
701 assert_eq!(patch.len(), 1);
702 assert!(
703 matches!(&patch.operations[0], GenericPatchOperation::Add { path, .. } if path == "/age")
704 );
705 }
706
707 #[test]
708 fn test_compute_diff_remove_field() {
709 let source = json!({"name": "Alice", "age": 30});
710 let target = json!({"name": "Alice"});
711
712 let patch = compute_diff(&source, &target);
713 assert_eq!(patch.len(), 1);
714 assert!(
715 matches!(&patch.operations[0], GenericPatchOperation::Remove { path } if path == "/age")
716 );
717 }
718
719 #[test]
720 fn test_compute_diff_array_add() {
721 let source = json!([1, 2]);
722 let target = json!([1, 2, 3]);
723
724 let patch = compute_diff(&source, &target);
725 assert_eq!(patch.len(), 1);
726 assert!(
727 matches!(&patch.operations[0], GenericPatchOperation::Add { path, .. } if path == "/2")
728 );
729 }
730
731 #[test]
732 fn test_compute_diff_array_remove() {
733 let source = json!([1, 2, 3]);
734 let target = json!([1, 2]);
735
736 let patch = compute_diff(&source, &target);
737 assert_eq!(patch.len(), 1);
738 assert!(
739 matches!(&patch.operations[0], GenericPatchOperation::Remove { path } if path == "/2")
740 );
741 }
742
743 #[test]
744 fn test_compute_diff_type_change() {
745 let source = json!({"value": "string"});
746 let target = json!({"value": 42});
747
748 let patch = compute_diff(&source, &target);
749 assert_eq!(patch.len(), 1);
750 assert!(patch.operations[0].is_replace());
751 }
752
753 #[test]
754 fn test_compute_diff_nested() {
755 let source = json!({"user": {"name": "Alice"}});
756 let target = json!({"user": {"name": "Bob"}});
757
758 let patch = compute_diff(&source, &target);
759 assert_eq!(patch.len(), 1);
760 assert!(
761 matches!(&patch.operations[0], GenericPatchOperation::Replace { path, .. } if path == "/user/name")
762 );
763 }
764
765 #[test]
766 fn test_generic_patch_operations() {
767 let op: GenericPatchOperation<serde_json::Value> = GenericPatchOperation::Add {
768 path: "/test".to_string(),
769 value: json!(42),
770 };
771
772 assert_eq!(op.path(), "/test");
773 assert!(op.is_add());
774 assert!(!op.is_remove());
775 }
776
777 #[test]
778 fn test_generic_patch_iter() {
779 let patch = GenericPatch::with_operations(vec![
780 GenericPatchOperation::Add {
781 path: "/a".to_string(),
782 value: json!(1),
783 },
784 GenericPatchOperation::Add {
785 path: "/b".to_string(),
786 value: json!(2),
787 },
788 ]);
789
790 assert_eq!(patch.len(), 2);
791 assert!(!patch.is_empty());
792
793 let paths: Vec<_> = patch.iter().map(GenericPatchOperation::path).collect();
794 assert_eq!(paths, vec!["/a", "/b"]);
795 }
796
797 #[test]
798 fn test_diff_options() {
799 let options = DiffOptions::new().with_move_detection().with_max_depth(5);
800
801 assert!(options.detect_moves);
802 assert_eq!(options.max_depth, 5);
803 }
804
805 #[test]
806 fn test_escape_json_pointer() {
807 assert_eq!(escape_json_pointer("simple"), "simple");
808 assert_eq!(escape_json_pointer("with/slash"), "with~1slash");
809 assert_eq!(escape_json_pointer("with~tilde"), "with~0tilde");
810 }
811}