1use fionn_core::Result;
29use fionn_core::tape_source::{TapeNodeKind, TapeSource, TapeValue};
30use std::borrow::Cow;
31
32#[derive(Debug, Clone, PartialEq)]
34pub enum TapeDiffOp<'a> {
35 Add {
37 path: String,
39 value: TapeValueOwned,
41 },
42 Remove {
44 path: String,
46 },
47 Replace {
49 path: String,
51 value: TapeValueOwned,
53 },
54 Move {
56 from: String,
58 path: String,
60 },
61 Copy {
63 from: String,
65 path: String,
67 },
68 AddRef {
70 path: String,
72 tape_index: usize,
74 _marker: std::marker::PhantomData<&'a ()>,
76 },
77 ReplaceRef {
79 path: String,
81 tape_index: usize,
83 _marker: std::marker::PhantomData<&'a ()>,
85 },
86}
87
88#[derive(Debug, Clone, PartialEq)]
90pub enum TapeValueOwned {
91 Null,
93 Bool(bool),
95 Int(i64),
97 Float(f64),
99 String(String),
101 RawNumber(String),
103 Json(String),
105}
106
107impl<'a> From<TapeValue<'a>> for TapeValueOwned {
108 fn from(val: TapeValue<'a>) -> Self {
109 match val {
110 TapeValue::Null => Self::Null,
111 TapeValue::Bool(b) => Self::Bool(b),
112 TapeValue::Int(n) => Self::Int(n),
113 TapeValue::Float(f) => Self::Float(f),
114 TapeValue::String(s) => Self::String(s.into_owned()),
115 TapeValue::RawNumber(s) => Self::RawNumber(s.into_owned()),
116 }
117 }
118}
119
120#[derive(Debug, Clone, Default)]
122pub struct TapeDiff<'a> {
123 pub operations: Vec<TapeDiffOp<'a>>,
125}
126
127impl<'a> TapeDiff<'a> {
128 #[must_use]
130 pub const fn new() -> Self {
131 Self {
132 operations: Vec::new(),
133 }
134 }
135
136 #[must_use]
138 pub const fn is_empty(&self) -> bool {
139 self.operations.is_empty()
140 }
141
142 #[must_use]
144 pub const fn len(&self) -> usize {
145 self.operations.len()
146 }
147
148 pub fn push(&mut self, op: TapeDiffOp<'a>) {
150 self.operations.push(op);
151 }
152}
153
154#[derive(Debug, Clone, Default)]
156pub struct TapeDiffOptions {
157 pub max_depth: usize,
159 pub use_refs: bool,
161}
162
163impl TapeDiffOptions {
164 #[must_use]
166 pub fn new() -> Self {
167 Self::default()
168 }
169
170 #[must_use]
172 pub const fn with_max_depth(mut self, depth: usize) -> Self {
173 self.max_depth = depth;
174 self
175 }
176
177 #[must_use]
179 pub const fn with_refs(mut self) -> Self {
180 self.use_refs = true;
181 self
182 }
183}
184
185pub fn diff_tapes<'a, S: TapeSource, T: TapeSource>(
193 source: &'a S,
194 target: &'a T,
195) -> Result<TapeDiff<'a>> {
196 diff_tapes_with_options(source, target, &TapeDiffOptions::default())
197}
198
199pub fn diff_tapes_with_options<'a, S: TapeSource, T: TapeSource>(
205 source: &'a S,
206 target: &'a T,
207 options: &TapeDiffOptions,
208) -> Result<TapeDiff<'a>> {
209 let mut diff = TapeDiff::new();
210
211 if source.is_empty() && target.is_empty() {
212 return Ok(diff);
213 }
214
215 if source.is_empty() {
216 let value = extract_value(target, 0)?;
218 diff.push(TapeDiffOp::Add {
219 path: String::new(),
220 value,
221 });
222 return Ok(diff);
223 }
224
225 if target.is_empty() {
226 diff.push(TapeDiffOp::Remove {
227 path: String::new(),
228 });
229 return Ok(diff);
230 }
231
232 diff_at_path(source, 0, target, 0, "", &mut diff, options, 0)?;
234
235 Ok(diff)
236}
237
238#[allow(clippy::too_many_arguments)] fn diff_at_path<S: TapeSource, T: TapeSource>(
240 source: &S,
241 src_idx: usize,
242 target: &T,
243 tgt_idx: usize,
244 path: &str,
245 diff: &mut TapeDiff<'_>,
246 options: &TapeDiffOptions,
247 depth: usize,
248) -> Result<()> {
249 if options.max_depth > 0 && depth >= options.max_depth {
251 if !values_equal(source, src_idx, target, tgt_idx) {
252 let value = extract_value(target, tgt_idx)?;
253 diff.push(TapeDiffOp::Replace {
254 path: path.to_string(),
255 value,
256 });
257 }
258 return Ok(());
259 }
260
261 let src_node = source.node_at(src_idx);
262 let tgt_node = target.node_at(tgt_idx);
263
264 match (src_node, tgt_node) {
265 (Some(src), Some(tgt)) => {
266 let src_kind_class = kind_class(&src.kind);
268 let tgt_kind_class = kind_class(&tgt.kind);
269
270 if src_kind_class != tgt_kind_class {
271 let value = extract_value(target, tgt_idx)?;
273 diff.push(TapeDiffOp::Replace {
274 path: path.to_string(),
275 value,
276 });
277 return Ok(());
278 }
279
280 match src.kind {
281 TapeNodeKind::ObjectStart { count: src_count } => {
282 if let TapeNodeKind::ObjectStart { count: tgt_count } = tgt.kind {
283 diff_objects(
284 source, src_idx, src_count, target, tgt_idx, tgt_count, path, diff,
285 options, depth,
286 )?;
287 }
288 }
289 TapeNodeKind::ArrayStart { count: src_count } => {
290 if let TapeNodeKind::ArrayStart { count: tgt_count } = tgt.kind {
291 diff_arrays(
292 source, src_idx, src_count, target, tgt_idx, tgt_count, path, diff,
293 options, depth,
294 )?;
295 }
296 }
297 TapeNodeKind::Value | TapeNodeKind::Key => {
298 if !values_equal(source, src_idx, target, tgt_idx) {
300 let value = extract_value(target, tgt_idx)?;
301 diff.push(TapeDiffOp::Replace {
302 path: path.to_string(),
303 value,
304 });
305 }
306 }
307 _ => {
308 }
310 }
311 }
312 (Some(_), None) => {
313 diff.push(TapeDiffOp::Remove {
315 path: path.to_string(),
316 });
317 }
318 (None, Some(_)) => {
319 let value = extract_value(target, tgt_idx)?;
321 diff.push(TapeDiffOp::Add {
322 path: path.to_string(),
323 value,
324 });
325 }
326 (None, None) => {
327 }
329 }
330
331 Ok(())
332}
333
334#[allow(clippy::too_many_arguments)] fn diff_objects<S: TapeSource, T: TapeSource>(
336 source: &S,
337 src_obj_idx: usize,
338 src_count: usize,
339 target: &T,
340 tgt_obj_idx: usize,
341 tgt_count: usize,
342 path: &str,
343 diff: &mut TapeDiff<'_>,
344 options: &TapeDiffOptions,
345 depth: usize,
346) -> Result<()> {
347 let src_fields = collect_object_fields(source, src_obj_idx, src_count)?;
349 let tgt_fields = collect_object_fields(target, tgt_obj_idx, tgt_count)?;
350
351 for (key, _src_val_idx) in &src_fields {
353 if !tgt_fields.iter().any(|(k, _)| k == key) {
354 let field_path = format_path(path, key);
355 diff.push(TapeDiffOp::Remove { path: field_path });
356 }
357 }
358
359 for (key, tgt_val_idx) in &tgt_fields {
361 let field_path = format_path(path, key);
362
363 if let Some((_k, src_val_idx)) = src_fields.iter().find(|(k, _)| k == key) {
364 diff_at_path(
366 source,
367 *src_val_idx,
368 target,
369 *tgt_val_idx,
370 &field_path,
371 diff,
372 options,
373 depth + 1,
374 )?;
375 } else {
376 let value = extract_value(target, *tgt_val_idx)?;
378 diff.push(TapeDiffOp::Add {
379 path: field_path,
380 value,
381 });
382 }
383 }
384
385 Ok(())
386}
387
388#[allow(clippy::too_many_arguments)] fn diff_arrays<S: TapeSource, T: TapeSource>(
390 source: &S,
391 src_arr_idx: usize,
392 src_count: usize,
393 target: &T,
394 tgt_arr_idx: usize,
395 tgt_count: usize,
396 path: &str,
397 diff: &mut TapeDiff<'_>,
398 options: &TapeDiffOptions,
399 depth: usize,
400) -> Result<()> {
401 let src_elements = collect_array_elements(source, src_arr_idx, src_count)?;
403 let tgt_elements = collect_array_elements(target, tgt_arr_idx, tgt_count)?;
404
405 let min_len = src_elements.len().min(tgt_elements.len());
406
407 for i in 0..min_len {
409 let elem_path = format!("{path}/{i}");
410 diff_at_path(
411 source,
412 src_elements[i],
413 target,
414 tgt_elements[i],
415 &elem_path,
416 diff,
417 options,
418 depth + 1,
419 )?;
420 }
421
422 for (i, &tgt_elem_idx) in tgt_elements.iter().enumerate().skip(min_len) {
424 let elem_path = format!("{path}/{i}");
425 let value = extract_value(target, tgt_elem_idx)?;
426 diff.push(TapeDiffOp::Add {
427 path: elem_path,
428 value,
429 });
430 }
431
432 for i in (min_len..src_elements.len()).rev() {
434 let elem_path = format!("{path}/{i}");
435 diff.push(TapeDiffOp::Remove { path: elem_path });
436 }
437
438 Ok(())
439}
440
441fn collect_object_fields<S: TapeSource>(
443 tape: &S,
444 obj_idx: usize,
445 count: usize,
446) -> Result<Vec<(Cow<'_, str>, usize)>> {
447 let mut fields = Vec::with_capacity(count);
448 let mut idx = obj_idx + 1;
449
450 for _ in 0..count {
451 if let Some(key) = tape.key_at(idx) {
453 let value_idx = idx + 1;
454 fields.push((key, value_idx));
455 idx = tape.skip_value(value_idx)?;
457 } else {
458 idx += 1;
459 }
460 }
461
462 Ok(fields)
463}
464
465fn collect_array_elements<S: TapeSource>(
467 tape: &S,
468 arr_idx: usize,
469 count: usize,
470) -> Result<Vec<usize>> {
471 let mut elements = Vec::with_capacity(count);
472 let mut idx = arr_idx + 1;
473
474 for _ in 0..count {
475 elements.push(idx);
476 idx = tape.skip_value(idx)?;
477 }
478
479 Ok(elements)
480}
481
482fn values_equal<S: TapeSource, T: TapeSource>(
484 source: &S,
485 src_idx: usize,
486 target: &T,
487 tgt_idx: usize,
488) -> bool {
489 let src_val = source.value_at(src_idx);
490 let tgt_val = target.value_at(tgt_idx);
491
492 match (src_val, tgt_val) {
493 (Some(s), Some(t)) => tape_values_equal(&s, &t),
494 (None, None) => {
495 let src_node = source.node_at(src_idx);
497 let tgt_node = target.node_at(tgt_idx);
498
499 match (src_node, tgt_node) {
500 (Some(s), Some(t)) => {
501 if kind_class(&s.kind) != kind_class(&t.kind) {
502 return false;
503 }
504
505 match (s.kind, t.kind) {
506 (
507 TapeNodeKind::ObjectStart { count: sc },
508 TapeNodeKind::ObjectStart { count: tc },
509 ) => {
510 if sc != tc {
511 return false;
512 }
513 false
516 }
517 (
518 TapeNodeKind::ArrayStart { count: sc },
519 TapeNodeKind::ArrayStart { count: tc },
520 ) => {
521 if sc != tc {
522 return false;
523 }
524 false
525 }
526 _ => false,
527 }
528 }
529 _ => false,
530 }
531 }
532 _ => false,
533 }
534}
535
536fn tape_values_equal(a: &TapeValue<'_>, b: &TapeValue<'_>) -> bool {
537 match (a, b) {
538 (TapeValue::Null, TapeValue::Null) => true,
539 (TapeValue::Bool(a), TapeValue::Bool(b)) => a == b,
540 (TapeValue::Int(a), TapeValue::Int(b)) => a == b,
541 (TapeValue::Float(a), TapeValue::Float(b)) => {
542 if a.is_nan() && b.is_nan() {
545 true
546 } else if a.is_infinite() || b.is_infinite() {
547 a.to_bits() == b.to_bits()
548 } else {
549 (a - b).abs() < f64::EPSILON
550 }
551 }
552 (TapeValue::String(a), TapeValue::String(b))
553 | (TapeValue::RawNumber(a), TapeValue::RawNumber(b)) => a == b,
554 (TapeValue::Int(a), TapeValue::Float(b)) => {
556 if b.is_nan() || b.is_infinite() {
557 false
558 } else {
559 (*a as f64 - b).abs() < f64::EPSILON
560 }
561 }
562 (TapeValue::Float(a), TapeValue::Int(b)) => {
563 if a.is_nan() || a.is_infinite() {
564 false
565 } else {
566 (a - *b as f64).abs() < f64::EPSILON
567 }
568 }
569 _ => false,
570 }
571}
572
573fn extract_value<T: TapeSource>(tape: &T, idx: usize) -> Result<TapeValueOwned> {
575 if let Some(val) = tape.value_at(idx) {
576 return Ok(val.into());
577 }
578
579 let node = tape.node_at(idx);
581 match node {
582 Some(n) => {
583 match n.kind {
584 TapeNodeKind::ObjectStart { .. } | TapeNodeKind::ArrayStart { .. } => {
585 let json = serialize_subtree(tape, idx)?;
587 Ok(TapeValueOwned::Json(json))
588 }
589 _ => Ok(TapeValueOwned::Null),
590 }
591 }
592 None => Ok(TapeValueOwned::Null),
593 }
594}
595
596fn serialize_subtree<T: TapeSource>(tape: &T, start_idx: usize) -> Result<String> {
598 let mut output = String::new();
599 serialize_value(tape, start_idx, &mut output)?;
600 Ok(output)
601}
602
603fn serialize_value<T: TapeSource>(tape: &T, idx: usize, output: &mut String) -> Result<usize> {
604 let node = tape.node_at(idx);
605
606 match node {
607 Some(n) => {
608 match n.kind {
609 TapeNodeKind::ObjectStart { count } => {
610 output.push('{');
611 let mut current_idx = idx + 1;
612 for i in 0..count {
613 if i > 0 {
614 output.push(',');
615 }
616 if let Some(key) = tape.key_at(current_idx) {
618 output.push('"');
619 output.push_str(&escape_json_string(&key));
620 output.push_str("\":");
621 current_idx += 1;
622 }
623 current_idx = serialize_value(tape, current_idx, output)?;
625 }
626 output.push('}');
627 Ok(current_idx + 1)
629 }
630 TapeNodeKind::ArrayStart { count } => {
631 output.push('[');
632 let mut current_idx = idx + 1;
633 for i in 0..count {
634 if i > 0 {
635 output.push(',');
636 }
637 current_idx = serialize_value(tape, current_idx, output)?;
638 }
639 output.push(']');
640 Ok(current_idx + 1)
642 }
643 TapeNodeKind::Value | TapeNodeKind::Key => {
644 if let Some(val) = tape.value_at(idx) {
645 serialize_tape_value(&val, output);
646 }
647 Ok(idx + 1)
648 }
649 TapeNodeKind::ObjectEnd | TapeNodeKind::ArrayEnd => Ok(idx + 1),
650 }
651 }
652 None => Ok(idx + 1),
653 }
654}
655
656fn serialize_tape_value(val: &TapeValue<'_>, output: &mut String) {
657 match val {
658 TapeValue::Null => output.push_str("null"),
659 TapeValue::Bool(true) => output.push_str("true"),
660 TapeValue::Bool(false) => output.push_str("false"),
661 TapeValue::Int(n) => output.push_str(&n.to_string()),
662 TapeValue::Float(f) => output.push_str(&f.to_string()),
663 TapeValue::String(s) => {
664 output.push('"');
665 output.push_str(&escape_json_string(s));
666 output.push('"');
667 }
668 TapeValue::RawNumber(s) => output.push_str(s),
669 }
670}
671
672fn escape_json_string(s: &str) -> Cow<'_, str> {
673 if s.bytes()
674 .any(|b| matches!(b, b'"' | b'\\' | b'\n' | b'\r' | b'\t'))
675 {
676 let escaped = s
677 .replace('\\', "\\\\")
678 .replace('"', "\\\"")
679 .replace('\n', "\\n")
680 .replace('\r', "\\r")
681 .replace('\t', "\\t");
682 Cow::Owned(escaped)
683 } else {
684 Cow::Borrowed(s)
685 }
686}
687
688const fn kind_class(kind: &TapeNodeKind) -> u8 {
690 match kind {
691 TapeNodeKind::ObjectStart { .. } | TapeNodeKind::ObjectEnd => 1,
692 TapeNodeKind::ArrayStart { .. } | TapeNodeKind::ArrayEnd => 2,
693 TapeNodeKind::Key | TapeNodeKind::Value => 3,
694 }
695}
696
697fn format_path(base: &str, key: &str) -> String {
698 if base.is_empty() {
699 format!("/{}", escape_json_pointer(key))
700 } else {
701 format!("{base}/{}", escape_json_pointer(key))
702 }
703}
704
705fn escape_json_pointer(s: &str) -> String {
706 s.replace('~', "~0").replace('/', "~1")
707}
708
709#[cfg(test)]
714mod tests {
715 use super::*;
716 use fionn_tape::DsonTape;
717
718 fn parse_json(s: &str) -> DsonTape {
719 DsonTape::parse(s).expect("valid JSON")
720 }
721
722 #[test]
723 fn test_diff_equal_objects() {
724 let a = parse_json(r#"{"name": "Alice", "age": 30}"#);
725 let b = parse_json(r#"{"name": "Alice", "age": 30}"#);
726
727 let diff = diff_tapes(&a, &b).unwrap();
728 assert!(diff.is_empty());
729 }
730
731 #[test]
732 fn test_diff_scalar_change() {
733 let a = parse_json(r#"{"name": "Alice"}"#);
734 let b = parse_json(r#"{"name": "Bob"}"#);
735
736 let diff = diff_tapes(&a, &b).unwrap();
737 assert_eq!(diff.len(), 1);
738 assert!(matches!(&diff.operations[0], TapeDiffOp::Replace { path, .. } if path == "/name"));
739 }
740
741 #[test]
742 fn test_diff_add_field() {
743 let a = parse_json(r#"{"name": "Alice"}"#);
744 let b = parse_json(r#"{"name": "Alice", "age": 30}"#);
745
746 let diff = diff_tapes(&a, &b).unwrap();
747 assert_eq!(diff.len(), 1);
748 assert!(matches!(&diff.operations[0], TapeDiffOp::Add { path, .. } if path == "/age"));
749 }
750
751 #[test]
752 fn test_diff_remove_field() {
753 let a = parse_json(r#"{"name": "Alice", "age": 30}"#);
754 let b = parse_json(r#"{"name": "Alice"}"#);
755
756 let diff = diff_tapes(&a, &b).unwrap();
757 assert_eq!(diff.len(), 1);
758 assert!(matches!(&diff.operations[0], TapeDiffOp::Remove { path } if path == "/age"));
759 }
760
761 #[test]
762 fn test_diff_nested_change() {
763 let a = parse_json(r#"{"user": {"name": "Alice"}}"#);
764 let b = parse_json(r#"{"user": {"name": "Bob"}}"#);
765
766 let diff = diff_tapes(&a, &b).unwrap();
767 assert_eq!(diff.len(), 1);
768 assert!(
769 matches!(&diff.operations[0], TapeDiffOp::Replace { path, .. } if path == "/user/name")
770 );
771 }
772
773 #[test]
774 fn test_diff_array_add() {
775 let a = parse_json(r"[1, 2]");
776 let b = parse_json(r"[1, 2, 3]");
777
778 let diff = diff_tapes(&a, &b).unwrap();
779 assert_eq!(diff.len(), 1);
780 assert!(matches!(&diff.operations[0], TapeDiffOp::Add { path, .. } if path == "/2"));
781 }
782
783 #[test]
784 fn test_diff_array_remove() {
785 let a = parse_json(r"[1, 2, 3]");
786 let b = parse_json(r"[1, 2]");
787
788 let diff = diff_tapes(&a, &b).unwrap();
789 assert_eq!(diff.len(), 1);
790 assert!(matches!(&diff.operations[0], TapeDiffOp::Remove { path } if path == "/2"));
791 }
792
793 #[test]
794 fn test_diff_type_change() {
795 let a = parse_json(r#"{"value": "string"}"#);
796 let b = parse_json(r#"{"value": 42}"#);
797
798 let diff = diff_tapes(&a, &b).unwrap();
799 assert_eq!(diff.len(), 1);
800 assert!(
801 matches!(&diff.operations[0], TapeDiffOp::Replace { path, .. } if path == "/value")
802 );
803 }
804
805 #[test]
806 fn test_diff_empty_to_object() {
807 let a = parse_json(r"{}");
808 let b = parse_json(r#"{"name": "Alice"}"#);
809
810 let diff = diff_tapes(&a, &b).unwrap();
811 assert_eq!(diff.len(), 1);
812 assert!(matches!(&diff.operations[0], TapeDiffOp::Add { .. }));
813 }
814
815 #[test]
816 fn test_diff_array_scalar_change() {
817 let a = parse_json(r"[1, 2, 3]");
819 let b = parse_json(r"[1, 99, 3]");
820
821 let diff = diff_tapes(&a, &b).unwrap();
822 assert_eq!(diff.len(), 1, "Expected exactly one change");
823 assert!(matches!(&diff.operations[0], TapeDiffOp::Replace { path, .. } if path == "/1"));
824 }
825
826 #[test]
827 fn test_diff_complex_nested() {
828 let a = parse_json(r#"[{"name": "Alice"}, {"name": "Bob"}]"#);
830 let b = parse_json(r#"[{"name": "Alice"}, {"name": "Carol"}]"#);
831
832 let diff = diff_tapes(&a, &b).unwrap();
833
834 assert_eq!(diff.len(), 1, "Expected exactly one change");
836 assert!(
837 matches!(&diff.operations[0], TapeDiffOp::Replace { path, .. } if path == "/1/name")
838 );
839 }
840
841 #[test]
842 fn test_diff_deeply_nested() {
843 let a = parse_json(r#"{"users": [{"profile": {"name": "Alice"}}]}"#);
844 let b = parse_json(r#"{"users": [{"profile": {"name": "Bob"}}]}"#);
845
846 let diff = diff_tapes(&a, &b).unwrap();
847 assert_eq!(diff.len(), 1);
848 assert!(
849 matches!(&diff.operations[0], TapeDiffOp::Replace { path, .. } if path == "/users/0/profile/name")
850 );
851 }
852
853 #[test]
854 fn test_serialize_subtree() {
855 let tape = parse_json(r#"{"nested": {"a": 1, "b": "hello"}}"#);
856 let json = serialize_subtree(&tape, 2).unwrap();
859 assert!(json.contains("\"a\":1") || json.contains("\"a\": 1"));
860 }
861}