1use std::borrow::Cow;
5use std::collections::{HashMap, HashSet};
6
7use facet::{Def, DynValueKind, StructKind, Type, UserType};
8use facet_core::Facet;
9use facet_diff_core::{Diff, Path, PathSegment, Updates, Value};
10use facet_reflect::{HasFields, Peek, ScalarType};
11
12use crate::sequences;
13
14#[derive(Debug, Clone, Default)]
16pub struct DiffOptions {
17 pub float_tolerance: Option<f64>,
21}
22
23impl DiffOptions {
24 pub fn new() -> Self {
26 Self::default()
27 }
28
29 pub fn with_float_tolerance(mut self, tolerance: f64) -> Self {
31 self.float_tolerance = Some(tolerance);
32 self
33 }
34}
35
36pub trait FacetDiff<'f>: Facet<'f> {
38 fn diff<'a, U: Facet<'f>>(&'a self, other: &'a U) -> Diff<'a, 'f>;
40}
41
42impl<'f, T: Facet<'f>> FacetDiff<'f> for T {
43 fn diff<'a, U: Facet<'f>>(&'a self, other: &'a U) -> Diff<'a, 'f> {
44 diff_new(self, other)
45 }
46}
47
48pub fn diff_new<'mem, 'facet, T: Facet<'facet>, U: Facet<'facet>>(
50 from: &'mem T,
51 to: &'mem U,
52) -> Diff<'mem, 'facet> {
53 diff_new_peek(Peek::new(from), Peek::new(to))
54}
55
56pub fn diff_new_peek_with_options<'mem, 'facet>(
58 from: Peek<'mem, 'facet>,
59 to: Peek<'mem, 'facet>,
60 options: &DiffOptions,
61) -> Diff<'mem, 'facet> {
62 let from = deref_if_pointer(from);
64 let to = deref_if_pointer(to);
65
66 let same_type = from.shape().type_identifier == to.shape().type_identifier;
70 let from_has_partialeq = from.shape().is_partial_eq();
71 let to_has_partialeq = to.shape().is_partial_eq();
72 let values_equal = from == to;
73
74 let float_equal = options
76 .float_tolerance
77 .map(|tol| check_float_tolerance(from, to, tol))
78 .unwrap_or(false);
79
80 if same_type && from_has_partialeq && to_has_partialeq && (values_equal || float_equal) {
90 return Diff::Equal { value: Some(from) };
91 }
92
93 match (
94 (from.shape().def, from.shape().ty),
95 (to.shape().def, to.shape().ty),
96 ) {
97 ((_, Type::User(UserType::Struct(from_ty))), (_, Type::User(UserType::Struct(to_ty))))
98 if from_ty.kind == to_ty.kind =>
99 {
100 let from_ty = from.into_struct().unwrap();
101 let to_ty = to.into_struct().unwrap();
102
103 let value = if [StructKind::Tuple, StructKind::TupleStruct].contains(&from_ty.ty().kind)
104 {
105 let from = from_ty.fields().map(|x| x.1).collect();
106 let to = to_ty.fields().map(|x| x.1).collect();
107
108 let updates = sequences::diff_with_options(from, to, options);
109
110 Value::Tuple { updates }
111 } else {
112 let mut updates = HashMap::new();
113 let mut deletions = HashMap::new();
114 let mut insertions = HashMap::new();
115 let mut unchanged = HashSet::new();
116
117 for (field, from) in from_ty.fields() {
118 if let Ok(to) = to_ty.field_by_name(field.name) {
119 let diff = diff_new_peek_with_options(from, to, options);
120 if diff.is_equal() {
121 unchanged.insert(Cow::Borrowed(field.name));
122 } else {
123 updates.insert(Cow::Borrowed(field.name), diff);
124 }
125 } else {
126 deletions.insert(Cow::Borrowed(field.name), from);
127 }
128 }
129
130 for (field, to) in to_ty.fields() {
131 if from_ty.field_by_name(field.name).is_err() {
132 insertions.insert(Cow::Borrowed(field.name), to);
133 }
134 }
135 Value::Struct {
136 updates,
137 deletions,
138 insertions,
139 unchanged,
140 }
141 };
142
143 let is_empty = match &value {
145 Value::Tuple { updates } => updates.is_empty(),
146 Value::Struct {
147 updates,
148 deletions,
149 insertions,
150 ..
151 } => updates.is_empty() && deletions.is_empty() && insertions.is_empty(),
152 };
153 if is_empty {
154 return Diff::Equal { value: Some(from) };
155 }
156
157 Diff::User {
158 from: from.shape(),
159 to: to.shape(),
160 variant: None,
161 value,
162 }
163 }
164 ((_, Type::User(UserType::Enum(_))), (_, Type::User(UserType::Enum(_)))) => {
165 let from_enum = from.into_enum().unwrap();
166 let to_enum = to.into_enum().unwrap();
167
168 let from_variant = from_enum.active_variant().unwrap();
169 let to_variant = to_enum.active_variant().unwrap();
170
171 if from_variant.name != to_variant.name
172 || from_variant.data.kind != to_variant.data.kind
173 {
174 return Diff::Replace { from, to };
175 }
176
177 let value =
178 if [StructKind::Tuple, StructKind::TupleStruct].contains(&from_variant.data.kind) {
179 let from = from_enum.fields().map(|x| x.1).collect();
180 let to = to_enum.fields().map(|x| x.1).collect();
181
182 let updates = sequences::diff_with_options(from, to, options);
183
184 Value::Tuple { updates }
185 } else {
186 let mut updates = HashMap::new();
187 let mut deletions = HashMap::new();
188 let mut insertions = HashMap::new();
189 let mut unchanged = HashSet::new();
190
191 for (field, from) in from_enum.fields() {
192 if let Ok(Some(to)) = to_enum.field_by_name(field.name) {
193 let diff = diff_new_peek_with_options(from, to, options);
194 if diff.is_equal() {
195 unchanged.insert(Cow::Borrowed(field.name));
196 } else {
197 updates.insert(Cow::Borrowed(field.name), diff);
198 }
199 } else {
200 deletions.insert(Cow::Borrowed(field.name), from);
201 }
202 }
203
204 for (field, to) in to_enum.fields() {
205 if !from_enum
206 .field_by_name(field.name)
207 .is_ok_and(|x| x.is_some())
208 {
209 insertions.insert(Cow::Borrowed(field.name), to);
210 }
211 }
212
213 Value::Struct {
214 updates,
215 deletions,
216 insertions,
217 unchanged,
218 }
219 };
220
221 let is_empty = match &value {
223 Value::Tuple { updates } => updates.is_empty(),
224 Value::Struct {
225 updates,
226 deletions,
227 insertions,
228 ..
229 } => updates.is_empty() && deletions.is_empty() && insertions.is_empty(),
230 };
231 if is_empty {
232 return Diff::Equal { value: Some(from) };
233 }
234
235 Diff::User {
236 from: from_enum.shape(),
237 to: to_enum.shape(),
238 variant: Some(from_variant.name),
239 value,
240 }
241 }
242 ((Def::Option(_), _), (Def::Option(_), _)) => {
243 let from_option = from.into_option().unwrap();
244 let to_option = to.into_option().unwrap();
245
246 let (Some(from_value), Some(to_value)) = (from_option.value(), to_option.value())
247 else {
248 return Diff::Replace { from, to };
249 };
250
251 let updates = sequences::diff_with_options(vec![from_value], vec![to_value], options);
253
254 if updates.is_empty() {
255 return Diff::Equal { value: Some(from) };
256 }
257
258 Diff::User {
259 from: from.shape(),
260 to: to.shape(),
261 variant: Some("Some"),
262 value: Value::Tuple { updates },
263 }
264 }
265 (
266 (Def::List(_) | Def::Slice(_), _) | (_, Type::Sequence(_)),
267 (Def::List(_) | Def::Slice(_), _) | (_, Type::Sequence(_)),
268 ) => {
269 let from_list = from.into_list_like().unwrap();
270 let to_list = to.into_list_like().unwrap();
271
272 let updates = sequences::diff_with_options(
273 from_list.iter().collect::<Vec<_>>(),
274 to_list.iter().collect::<Vec<_>>(),
275 options,
276 );
277
278 if updates.is_empty() {
279 return Diff::Equal { value: Some(from) };
280 }
281
282 Diff::Sequence {
283 from: from.shape(),
284 to: to.shape(),
285 updates,
286 }
287 }
288 ((Def::DynamicValue(_), _), (Def::DynamicValue(_), _)) => {
289 diff_dynamic_values(from, to, options)
290 }
291 ((Def::DynamicValue(_), _), _) => diff_dynamic_vs_concrete(from, to, false, options),
293 (_, (Def::DynamicValue(_), _)) => diff_dynamic_vs_concrete(to, from, true, options),
294 _ => Diff::Replace { from, to },
295 }
296}
297
298pub fn diff_new_peek<'mem, 'facet>(
300 from: Peek<'mem, 'facet>,
301 to: Peek<'mem, 'facet>,
302) -> Diff<'mem, 'facet> {
303 diff_new_peek_with_options(from, to, &DiffOptions::default())
304}
305
306fn diff_dynamic_values<'mem, 'facet>(
308 from: Peek<'mem, 'facet>,
309 to: Peek<'mem, 'facet>,
310 options: &DiffOptions,
311) -> Diff<'mem, 'facet> {
312 let from_dyn = from.into_dynamic_value().unwrap();
313 let to_dyn = to.into_dynamic_value().unwrap();
314
315 let from_kind = from_dyn.kind();
316 let to_kind = to_dyn.kind();
317
318 if from_kind != to_kind {
320 return Diff::Replace { from, to };
321 }
322
323 match from_kind {
324 DynValueKind::Null => Diff::Equal { value: Some(from) },
325 DynValueKind::Bool => {
326 if from_dyn.as_bool() == to_dyn.as_bool() {
327 Diff::Equal { value: Some(from) }
328 } else {
329 Diff::Replace { from, to }
330 }
331 }
332 DynValueKind::Number => {
333 let same = match (from_dyn.as_i64(), to_dyn.as_i64()) {
335 (Some(l), Some(r)) => l == r,
336 _ => match (from_dyn.as_u64(), to_dyn.as_u64()) {
337 (Some(l), Some(r)) => l == r,
338 _ => match (from_dyn.as_f64(), to_dyn.as_f64()) {
339 (Some(l), Some(r)) => l == r,
340 _ => false,
341 },
342 },
343 };
344 if same {
345 Diff::Equal { value: Some(from) }
346 } else {
347 Diff::Replace { from, to }
348 }
349 }
350 DynValueKind::String => {
351 if from_dyn.as_str() == to_dyn.as_str() {
352 Diff::Equal { value: Some(from) }
353 } else {
354 Diff::Replace { from, to }
355 }
356 }
357 DynValueKind::Bytes => {
358 if from_dyn.as_bytes() == to_dyn.as_bytes() {
359 Diff::Equal { value: Some(from) }
360 } else {
361 Diff::Replace { from, to }
362 }
363 }
364 DynValueKind::Array => {
365 let from_iter = from_dyn.array_iter();
367 let to_iter = to_dyn.array_iter();
368
369 let from_elems: Vec<_> = from_iter.map(|i| i.collect()).unwrap_or_default();
370 let to_elems: Vec<_> = to_iter.map(|i| i.collect()).unwrap_or_default();
371
372 let updates = sequences::diff_with_options(from_elems, to_elems, options);
373
374 if updates.is_empty() {
375 return Diff::Equal { value: Some(from) };
376 }
377
378 Diff::Sequence {
379 from: from.shape(),
380 to: to.shape(),
381 updates,
382 }
383 }
384 DynValueKind::Object => {
385 let from_len = from_dyn.object_len().unwrap_or(0);
387 let to_len = to_dyn.object_len().unwrap_or(0);
388
389 let mut updates = HashMap::new();
390 let mut deletions = HashMap::new();
391 let mut insertions = HashMap::new();
392 let mut unchanged = HashSet::new();
393
394 let mut from_keys: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
396 for i in 0..from_len {
397 if let Some((key, value)) = from_dyn.object_get_entry(i) {
398 from_keys.insert(key.to_owned(), value);
399 }
400 }
401
402 let mut to_keys: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
404 for i in 0..to_len {
405 if let Some((key, value)) = to_dyn.object_get_entry(i) {
406 to_keys.insert(key.to_owned(), value);
407 }
408 }
409
410 for (key, from_value) in &from_keys {
412 if let Some(to_value) = to_keys.get(key) {
413 let diff = diff_new_peek_with_options(*from_value, *to_value, options);
414 if diff.is_equal() {
415 unchanged.insert(Cow::Owned(key.clone()));
416 } else {
417 updates.insert(Cow::Owned(key.clone()), diff);
418 }
419 } else {
420 deletions.insert(Cow::Owned(key.clone()), *from_value);
421 }
422 }
423
424 for (key, to_value) in &to_keys {
425 if !from_keys.contains_key(key) {
426 insertions.insert(Cow::Owned(key.clone()), *to_value);
427 }
428 }
429
430 let is_empty = updates.is_empty() && deletions.is_empty() && insertions.is_empty();
431 if is_empty {
432 return Diff::Equal { value: Some(from) };
433 }
434
435 Diff::User {
436 from: from.shape(),
437 to: to.shape(),
438 variant: None,
439 value: Value::Struct {
440 updates,
441 deletions,
442 insertions,
443 unchanged,
444 },
445 }
446 }
447 DynValueKind::DateTime => {
448 if from_dyn.as_datetime() == to_dyn.as_datetime() {
450 Diff::Equal { value: Some(from) }
451 } else {
452 Diff::Replace { from, to }
453 }
454 }
455 DynValueKind::QName | DynValueKind::Uuid => {
456 Diff::Replace { from, to }
459 }
460 }
461}
462
463fn diff_dynamic_vs_concrete<'mem, 'facet>(
467 dyn_peek: Peek<'mem, 'facet>,
468 concrete_peek: Peek<'mem, 'facet>,
469 swapped: bool,
470 options: &DiffOptions,
471) -> Diff<'mem, 'facet> {
472 let (from_peek, to_peek) = if swapped {
474 (concrete_peek, dyn_peek)
475 } else {
476 (dyn_peek, concrete_peek)
477 };
478 let dyn_val = dyn_peek.into_dynamic_value().unwrap();
479 let dyn_kind = dyn_val.kind();
480
481 match dyn_kind {
483 DynValueKind::Bool => {
484 if concrete_peek
485 .get::<bool>()
486 .ok()
487 .is_some_and(|&v| dyn_val.as_bool() == Some(v))
488 {
489 return Diff::Equal {
490 value: Some(from_peek),
491 };
492 }
493 }
494 DynValueKind::Number => {
495 let is_equal =
496 concrete_peek.get::<i8>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
498 || concrete_peek.get::<i16>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
499 || concrete_peek.get::<i32>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
500 || concrete_peek.get::<i64>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v))
501 || concrete_peek.get::<isize>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
502 || concrete_peek.get::<u8>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
504 || concrete_peek.get::<u16>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
505 || concrete_peek.get::<u32>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
506 || concrete_peek.get::<u64>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v))
507 || concrete_peek.get::<usize>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
508 || concrete_peek.get::<f32>().ok().is_some_and(|&v| dyn_val.as_f64() == Some(v as f64))
510 || concrete_peek.get::<f64>().ok().is_some_and(|&v| dyn_val.as_f64() == Some(v));
511 if is_equal {
512 return Diff::Equal {
513 value: Some(from_peek),
514 };
515 }
516 }
517 DynValueKind::String => {
518 if concrete_peek
519 .as_str()
520 .is_some_and(|s| dyn_val.as_str() == Some(s))
521 {
522 return Diff::Equal {
523 value: Some(from_peek),
524 };
525 }
526 }
527 DynValueKind::Array => {
528 if let Ok(concrete_list) = concrete_peek.into_list_like() {
530 let dyn_elems: Vec<_> = dyn_val
531 .array_iter()
532 .map(|i| i.collect())
533 .unwrap_or_default();
534 let concrete_elems: Vec<_> = concrete_list.iter().collect();
535
536 let (from_elems, to_elems) = if swapped {
538 (concrete_elems, dyn_elems)
539 } else {
540 (dyn_elems, concrete_elems)
541 };
542 let updates = sequences::diff_with_options(from_elems, to_elems, options);
543
544 if updates.is_empty() {
545 return Diff::Equal {
546 value: Some(from_peek),
547 };
548 }
549
550 return Diff::Sequence {
551 from: from_peek.shape(),
552 to: to_peek.shape(),
553 updates,
554 };
555 }
556 }
557 DynValueKind::Object => {
558 if let Ok(concrete_struct) = concrete_peek.into_struct() {
560 let dyn_len = dyn_val.object_len().unwrap_or(0);
561
562 let mut updates = HashMap::new();
563 let mut deletions = HashMap::new();
564 let mut insertions = HashMap::new();
565 let mut unchanged = HashSet::new();
566
567 let mut dyn_keys: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
569 for i in 0..dyn_len {
570 if let Some((key, value)) = dyn_val.object_get_entry(i) {
571 dyn_keys.insert(key.to_owned(), value);
572 }
573 }
574
575 for (key, dyn_value) in &dyn_keys {
578 if let Ok(concrete_value) = concrete_struct.field_by_name(key) {
579 let diff = if swapped {
580 diff_new_peek_with_options(concrete_value, *dyn_value, options)
581 } else {
582 diff_new_peek_with_options(*dyn_value, concrete_value, options)
583 };
584 if diff.is_equal() {
585 unchanged.insert(Cow::Owned(key.clone()));
586 } else {
587 updates.insert(Cow::Owned(key.clone()), diff);
588 }
589 } else {
590 if swapped {
594 insertions.insert(Cow::Owned(key.clone()), *dyn_value);
595 } else {
596 deletions.insert(Cow::Owned(key.clone()), *dyn_value);
597 }
598 }
599 }
600
601 for (field, concrete_value) in concrete_struct.fields() {
602 if !dyn_keys.contains_key(field.name) {
603 if swapped {
607 deletions.insert(Cow::Borrowed(field.name), concrete_value);
608 } else {
609 insertions.insert(Cow::Borrowed(field.name), concrete_value);
610 }
611 }
612 }
613
614 let is_empty = updates.is_empty() && deletions.is_empty() && insertions.is_empty();
615 if is_empty {
616 return Diff::Equal {
617 value: Some(from_peek),
618 };
619 }
620
621 return Diff::User {
622 from: from_peek.shape(),
623 to: to_peek.shape(),
624 variant: None,
625 value: Value::Struct {
626 updates,
627 deletions,
628 insertions,
629 unchanged,
630 },
631 };
632 }
633 }
634 _ => {}
636 }
637
638 Diff::Replace {
639 from: from_peek,
640 to: to_peek,
641 }
642}
643
644fn try_extract_float(peek: Peek) -> Option<f64> {
646 match peek.scalar_type()? {
647 ScalarType::F64 => Some(*peek.get::<f64>().ok()?),
648 ScalarType::F32 => Some(*peek.get::<f32>().ok()? as f64),
649 _ => None,
650 }
651}
652
653fn check_float_tolerance(from: Peek, to: Peek, tolerance: f64) -> bool {
655 match (try_extract_float(from), try_extract_float(to)) {
656 (Some(f1), Some(f2)) => (f1 - f2).abs() <= tolerance,
657 _ => false,
658 }
659}
660
661fn deref_if_pointer<'mem, 'facet>(peek: Peek<'mem, 'facet>) -> Peek<'mem, 'facet> {
663 if let Ok(ptr) = peek.into_pointer()
664 && let Some(target) = ptr.borrow_inner()
665 {
666 return deref_if_pointer(target);
667 }
668 peek
669}
670
671pub fn collect_leaf_changes<'mem, 'facet>(
678 diff: &Diff<'mem, 'facet>,
679) -> Vec<LeafChange<'mem, 'facet>> {
680 let mut changes = Vec::new();
681 collect_leaf_changes_inner(diff, Path::new(), &mut changes);
682 changes
683}
684
685fn collect_leaf_changes_inner<'mem, 'facet>(
686 diff: &Diff<'mem, 'facet>,
687 path: Path,
688 changes: &mut Vec<LeafChange<'mem, 'facet>>,
689) {
690 match diff {
691 Diff::Equal { .. } => {
692 }
694 Diff::Replace { from, to } => {
695 changes.push(LeafChange {
697 path,
698 kind: LeafChangeKind::Replace {
699 from: *from,
700 to: *to,
701 },
702 });
703 }
704 Diff::User {
705 value,
706 variant,
707 from,
708 ..
709 } => {
710 let is_option = matches!(from.def, Def::Option(_));
713
714 let base_path = if let Some(v) = variant {
715 if is_option && *v == "Some" {
716 path } else {
718 path.with(PathSegment::Variant(Cow::Borrowed(*v)))
719 }
720 } else {
721 path
722 };
723
724 match value {
725 Value::Struct {
726 updates,
727 deletions,
728 insertions,
729 ..
730 } => {
731 for (field, diff) in updates {
733 let field_path = base_path.with(PathSegment::Field(field.clone()));
734 collect_leaf_changes_inner(diff, field_path, changes);
735 }
736 for (field, peek) in deletions {
738 let field_path = base_path.with(PathSegment::Field(field.clone()));
739 changes.push(LeafChange {
740 path: field_path,
741 kind: LeafChangeKind::Delete { value: *peek },
742 });
743 }
744 for (field, peek) in insertions {
746 let field_path = base_path.with(PathSegment::Field(field.clone()));
747 changes.push(LeafChange {
748 path: field_path,
749 kind: LeafChangeKind::Insert { value: *peek },
750 });
751 }
752 }
753 Value::Tuple { updates } => {
754 if is_option {
756 collect_from_updates_for_single_elem(&base_path, updates, changes);
758 } else {
759 collect_from_updates(&base_path, updates, changes);
760 }
761 }
762 }
763 }
764 Diff::Sequence { updates, .. } => {
765 collect_from_updates(&path, updates, changes);
766 }
767 }
768}
769
770fn collect_from_updates_for_single_elem<'mem, 'facet>(
773 base_path: &Path,
774 updates: &Updates<'mem, 'facet>,
775 changes: &mut Vec<LeafChange<'mem, 'facet>>,
776) {
777 if let Some(update_group) = &updates.0.first {
780 if let Some(replace) = &update_group.0.first
782 && replace.removals.len() == 1
783 && replace.additions.len() == 1
784 {
785 let from = replace.removals[0];
786 let to = replace.additions[0];
787 let nested = diff_new_peek(from, to);
788 if matches!(nested, Diff::Replace { .. }) {
789 changes.push(LeafChange {
790 path: base_path.clone(),
791 kind: LeafChangeKind::Replace { from, to },
792 });
793 } else {
794 collect_leaf_changes_inner(&nested, base_path.clone(), changes);
795 }
796 return;
797 }
798 if let Some(diffs) = &update_group.0.last {
800 for diff in diffs {
801 collect_leaf_changes_inner(diff, base_path.clone(), changes);
802 }
803 return;
804 }
805 }
806 collect_from_updates(base_path, updates, changes);
808}
809
810fn collect_from_updates<'mem, 'facet>(
811 base_path: &Path,
812 updates: &Updates<'mem, 'facet>,
813 changes: &mut Vec<LeafChange<'mem, 'facet>>,
814) {
815 let mut index = 0;
817
818 if let Some(update_group) = &updates.0.first {
820 collect_from_update_group(base_path, update_group, &mut index, changes);
821 }
822
823 for (unchanged, update_group) in &updates.0.values {
825 index += unchanged.len();
826 collect_from_update_group(base_path, update_group, &mut index, changes);
827 }
828
829 }
831
832fn collect_from_update_group<'mem, 'facet>(
833 base_path: &Path,
834 group: &crate::UpdatesGroup<'mem, 'facet>,
835 index: &mut usize,
836 changes: &mut Vec<LeafChange<'mem, 'facet>>,
837) {
838 if let Some(replace) = &group.0.first {
840 collect_from_replace_group(base_path, replace, index, changes);
841 }
842
843 for (diffs, replace) in &group.0.values {
845 for diff in diffs {
846 let elem_path = base_path.with(PathSegment::Index(*index));
847 collect_leaf_changes_inner(diff, elem_path, changes);
848 *index += 1;
849 }
850 collect_from_replace_group(base_path, replace, index, changes);
851 }
852
853 if let Some(diffs) = &group.0.last {
855 for diff in diffs {
856 let elem_path = base_path.with(PathSegment::Index(*index));
857 collect_leaf_changes_inner(diff, elem_path, changes);
858 *index += 1;
859 }
860 }
861}
862
863fn collect_from_replace_group<'mem, 'facet>(
864 base_path: &Path,
865 group: &crate::ReplaceGroup<'mem, 'facet>,
866 index: &mut usize,
867 changes: &mut Vec<LeafChange<'mem, 'facet>>,
868) {
869 if group.removals.len() == group.additions.len() {
874 for (from, to) in group.removals.iter().zip(group.additions.iter()) {
876 let elem_path = base_path.with(PathSegment::Index(*index));
877 let nested = diff_new_peek(*from, *to);
879 if matches!(nested, Diff::Replace { .. }) {
880 changes.push(LeafChange {
881 path: elem_path,
882 kind: LeafChangeKind::Replace {
883 from: *from,
884 to: *to,
885 },
886 });
887 } else {
888 collect_leaf_changes_inner(&nested, elem_path, changes);
889 }
890 *index += 1;
891 }
892 } else {
893 for from in &group.removals {
895 let elem_path = base_path.with(PathSegment::Index(*index));
896 changes.push(LeafChange {
897 path: elem_path.clone(),
898 kind: LeafChangeKind::Delete { value: *from },
899 });
900 *index += 1;
901 }
902 for to in &group.additions {
904 let elem_path = base_path.with(PathSegment::Index(*index));
905 changes.push(LeafChange {
906 path: elem_path,
907 kind: LeafChangeKind::Insert { value: *to },
908 });
909 *index += 1;
910 }
911 }
912}
913
914#[derive(Debug, Clone)]
916pub struct LeafChange<'mem, 'facet> {
917 pub path: Path,
919 pub kind: LeafChangeKind<'mem, 'facet>,
921}
922
923#[derive(Debug, Clone)]
925pub enum LeafChangeKind<'mem, 'facet> {
926 Replace {
928 from: Peek<'mem, 'facet>,
930 to: Peek<'mem, 'facet>,
932 },
933 Delete {
935 value: Peek<'mem, 'facet>,
937 },
938 Insert {
940 value: Peek<'mem, 'facet>,
942 },
943}
944
945impl<'mem, 'facet> LeafChange<'mem, 'facet> {
946 pub fn format_plain(&self) -> String {
948 use facet_pretty::PrettyPrinter;
949
950 let printer = PrettyPrinter::default()
951 .with_colors(false)
952 .with_minimal_option_names(true);
953
954 let mut out = String::new();
955
956 if !self.path.0.is_empty() {
958 out.push_str(&format!("{}: ", self.path));
959 }
960
961 match &self.kind {
962 LeafChangeKind::Replace { from, to } => {
963 out.push_str(&format!(
964 "{} → {}",
965 printer.format_peek(*from),
966 printer.format_peek(*to)
967 ));
968 }
969 LeafChangeKind::Delete { value } => {
970 out.push_str(&format!("- {}", printer.format_peek(*value)));
971 }
972 LeafChangeKind::Insert { value } => {
973 out.push_str(&format!("+ {}", printer.format_peek(*value)));
974 }
975 }
976
977 out
978 }
979
980 pub fn format_colored(&self) -> String {
982 use facet_pretty::{PrettyPrinter, tokyo_night};
983 use owo_colors::OwoColorize;
984
985 let printer = PrettyPrinter::default()
986 .with_colors(false)
987 .with_minimal_option_names(true);
988
989 let mut out = String::new();
990
991 if !self.path.0.is_empty() {
993 out.push_str(&format!(
994 "{}: ",
995 format!("{}", self.path).color(tokyo_night::FIELD_NAME)
996 ));
997 }
998
999 match &self.kind {
1000 LeafChangeKind::Replace { from, to } => {
1001 out.push_str(&format!(
1002 "{} {} {}",
1003 printer.format_peek(*from).color(tokyo_night::DELETION),
1004 "→".color(tokyo_night::COMMENT),
1005 printer.format_peek(*to).color(tokyo_night::INSERTION)
1006 ));
1007 }
1008 LeafChangeKind::Delete { value } => {
1009 out.push_str(&format!(
1010 "{} {}",
1011 "-".color(tokyo_night::DELETION),
1012 printer.format_peek(*value).color(tokyo_night::DELETION)
1013 ));
1014 }
1015 LeafChangeKind::Insert { value } => {
1016 out.push_str(&format!(
1017 "{} {}",
1018 "+".color(tokyo_night::INSERTION),
1019 printer.format_peek(*value).color(tokyo_night::INSERTION)
1020 ));
1021 }
1022 }
1023
1024 out
1025 }
1026}
1027
1028impl<'mem, 'facet> std::fmt::Display for LeafChange<'mem, 'facet> {
1029 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1030 write!(f, "{}", self.format_plain())
1031 }
1032}
1033
1034#[derive(Debug, Clone)]
1036pub struct DiffFormat {
1037 pub colors: bool,
1039 pub max_inline_changes: usize,
1041 pub prefer_compact: bool,
1043}
1044
1045impl Default for DiffFormat {
1046 fn default() -> Self {
1047 Self {
1048 colors: true,
1049 max_inline_changes: 10,
1050 prefer_compact: true,
1051 }
1052 }
1053}
1054
1055pub fn format_diff(diff: &Diff<'_, '_>, config: &DiffFormat) -> String {
1060 if matches!(diff, Diff::Equal { .. }) {
1061 return if config.colors {
1062 use facet_pretty::tokyo_night;
1063 use owo_colors::OwoColorize;
1064 "(no changes)".color(tokyo_night::MUTED).to_string()
1065 } else {
1066 "(no changes)".to_string()
1067 };
1068 }
1069
1070 let changes = collect_leaf_changes(diff);
1071
1072 if changes.is_empty() {
1073 return if config.colors {
1074 use facet_pretty::tokyo_night;
1075 use owo_colors::OwoColorize;
1076 "(no changes)".color(tokyo_night::MUTED).to_string()
1077 } else {
1078 "(no changes)".to_string()
1079 };
1080 }
1081
1082 if config.prefer_compact && changes.len() <= config.max_inline_changes {
1084 let mut out = String::new();
1085 for (i, change) in changes.iter().enumerate() {
1086 if i > 0 {
1087 out.push('\n');
1088 }
1089 if config.colors {
1090 out.push_str(&change.format_colored());
1091 } else {
1092 out.push_str(&change.format_plain());
1093 }
1094 }
1095 return out;
1096 }
1097
1098 if changes.len() > config.max_inline_changes {
1100 let mut out = String::new();
1101
1102 for (i, change) in changes.iter().take(config.max_inline_changes).enumerate() {
1104 if i > 0 {
1105 out.push('\n');
1106 }
1107 if config.colors {
1108 out.push_str(&change.format_colored());
1109 } else {
1110 out.push_str(&change.format_plain());
1111 }
1112 }
1113
1114 let remaining = changes.len() - config.max_inline_changes;
1116 if remaining > 0 {
1117 out.push('\n');
1118 let summary = format!(
1119 "... and {} more change{}",
1120 remaining,
1121 if remaining == 1 { "" } else { "s" }
1122 );
1123 if config.colors {
1124 use facet_pretty::tokyo_night;
1125 use owo_colors::OwoColorize;
1126 out.push_str(&summary.color(tokyo_night::MUTED).to_string());
1127 } else {
1128 out.push_str(&summary);
1129 }
1130 }
1131 return out;
1132 }
1133
1134 format!("{diff}")
1136}
1137
1138pub fn format_diff_default(diff: &Diff<'_, '_>) -> String {
1140 format_diff(diff, &DiffFormat::default())
1141}
1142
1143pub fn format_diff_compact(diff: &Diff<'_, '_>) -> String {
1145 format_diff(
1146 diff,
1147 &DiffFormat {
1148 prefer_compact: true,
1149 max_inline_changes: usize::MAX,
1150 ..Default::default()
1151 },
1152 )
1153}
1154
1155pub fn format_diff_compact_plain(diff: &Diff<'_, '_>) -> String {
1157 format_diff(
1158 diff,
1159 &DiffFormat {
1160 colors: false,
1161 prefer_compact: true,
1162 max_inline_changes: usize::MAX,
1163 },
1164 )
1165}