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 pub similarity_threshold: Option<f64>,
29}
30
31impl DiffOptions {
32 pub fn new() -> Self {
34 Self::default()
35 }
36
37 pub fn with_float_tolerance(mut self, tolerance: f64) -> Self {
39 self.float_tolerance = Some(tolerance);
40 self
41 }
42
43 pub fn with_similarity_threshold(mut self, threshold: f64) -> Self {
54 self.similarity_threshold = Some(threshold);
55 self
56 }
57}
58
59pub trait FacetDiff<'f>: Facet<'f> {
61 fn diff<'a, U: Facet<'f>>(&'a self, other: &'a U) -> Diff<'a, 'f>;
63}
64
65impl<'f, T: Facet<'f>> FacetDiff<'f> for T {
66 fn diff<'a, U: Facet<'f>>(&'a self, other: &'a U) -> Diff<'a, 'f> {
67 diff_new(self, other)
68 }
69}
70
71pub fn diff_new<'mem, 'facet, T: Facet<'facet>, U: Facet<'facet>>(
73 from: &'mem T,
74 to: &'mem U,
75) -> Diff<'mem, 'facet> {
76 diff_new_peek(Peek::new(from), Peek::new(to))
77}
78
79pub fn diff_new_peek_with_options<'mem, 'facet>(
81 from: Peek<'mem, 'facet>,
82 to: Peek<'mem, 'facet>,
83 options: &DiffOptions,
84) -> Diff<'mem, 'facet> {
85 let from = deref_if_pointer(from);
87 let to = deref_if_pointer(to);
88
89 let same_type = from.shape().type_identifier == to.shape().type_identifier;
93 let from_has_partialeq = from.shape().is_partial_eq();
94 let to_has_partialeq = to.shape().is_partial_eq();
95 let values_equal = from == to;
96
97 let float_equal = options
99 .float_tolerance
100 .map(|tol| check_float_tolerance(from, to, tol))
101 .unwrap_or(false);
102
103 if same_type && from_has_partialeq && to_has_partialeq && (values_equal || float_equal) {
113 return Diff::Equal { value: Some(from) };
114 }
115
116 match (
117 (from.shape().def, from.shape().ty),
118 (to.shape().def, to.shape().ty),
119 ) {
120 ((_, Type::User(UserType::Struct(from_ty))), (_, Type::User(UserType::Struct(to_ty))))
121 if from_ty.kind == to_ty.kind =>
122 {
123 let from_ty = from.into_struct().unwrap();
124 let to_ty = to.into_struct().unwrap();
125
126 let value = if [StructKind::Tuple, StructKind::TupleStruct].contains(&from_ty.ty().kind)
127 {
128 let from = from_ty.fields().map(|x| x.1).collect();
129 let to = to_ty.fields().map(|x| x.1).collect();
130
131 let updates = sequences::diff_with_options(from, to, options);
132
133 Value::Tuple { updates }
134 } else {
135 let mut updates = HashMap::new();
136 let mut deletions = HashMap::new();
137 let mut insertions = HashMap::new();
138 let mut unchanged = HashSet::new();
139
140 for (field, from) in from_ty.fields() {
141 if let Ok(to) = to_ty.field_by_name(field.name) {
142 let diff = diff_new_peek_with_options(from, to, options);
143 if diff.is_equal() {
144 unchanged.insert(Cow::Borrowed(field.name));
145 } else {
146 updates.insert(Cow::Borrowed(field.name), diff);
147 }
148 } else {
149 deletions.insert(Cow::Borrowed(field.name), from);
150 }
151 }
152
153 for (field, to) in to_ty.fields() {
154 if from_ty.field_by_name(field.name).is_err() {
155 insertions.insert(Cow::Borrowed(field.name), to);
156 }
157 }
158 Value::Struct {
159 updates,
160 deletions,
161 insertions,
162 unchanged,
163 }
164 };
165
166 let is_empty = match &value {
168 Value::Tuple { updates } => updates.is_empty(),
169 Value::Struct {
170 updates,
171 deletions,
172 insertions,
173 ..
174 } => updates.is_empty() && deletions.is_empty() && insertions.is_empty(),
175 };
176 if is_empty {
177 return Diff::Equal { value: Some(from) };
178 }
179
180 Diff::User {
181 from: from.shape(),
182 to: to.shape(),
183 variant: None,
184 value,
185 }
186 }
187 ((_, Type::User(UserType::Enum(_))), (_, Type::User(UserType::Enum(_)))) => {
188 let from_enum = from.into_enum().unwrap();
189 let to_enum = to.into_enum().unwrap();
190
191 let from_variant = from_enum.active_variant().unwrap();
192 let to_variant = to_enum.active_variant().unwrap();
193
194 if from_variant.name != to_variant.name
195 || from_variant.data.kind != to_variant.data.kind
196 {
197 return Diff::Replace { from, to };
198 }
199
200 let value =
201 if [StructKind::Tuple, StructKind::TupleStruct].contains(&from_variant.data.kind) {
202 let from = from_enum.fields().map(|x| x.1).collect();
203 let to = to_enum.fields().map(|x| x.1).collect();
204
205 let updates = sequences::diff_with_options(from, to, options);
206
207 Value::Tuple { updates }
208 } else {
209 let mut updates = HashMap::new();
210 let mut deletions = HashMap::new();
211 let mut insertions = HashMap::new();
212 let mut unchanged = HashSet::new();
213
214 for (field, from) in from_enum.fields() {
215 if let Ok(Some(to)) = to_enum.field_by_name(field.name) {
216 let diff = diff_new_peek_with_options(from, to, options);
217 if diff.is_equal() {
218 unchanged.insert(Cow::Borrowed(field.name));
219 } else {
220 updates.insert(Cow::Borrowed(field.name), diff);
221 }
222 } else {
223 deletions.insert(Cow::Borrowed(field.name), from);
224 }
225 }
226
227 for (field, to) in to_enum.fields() {
228 if !from_enum
229 .field_by_name(field.name)
230 .is_ok_and(|x| x.is_some())
231 {
232 insertions.insert(Cow::Borrowed(field.name), to);
233 }
234 }
235
236 Value::Struct {
237 updates,
238 deletions,
239 insertions,
240 unchanged,
241 }
242 };
243
244 let is_empty = match &value {
246 Value::Tuple { updates } => updates.is_empty(),
247 Value::Struct {
248 updates,
249 deletions,
250 insertions,
251 ..
252 } => updates.is_empty() && deletions.is_empty() && insertions.is_empty(),
253 };
254 if is_empty {
255 return Diff::Equal { value: Some(from) };
256 }
257
258 Diff::User {
259 from: from_enum.shape(),
260 to: to_enum.shape(),
261 variant: Some(from_variant.name),
262 value,
263 }
264 }
265 ((Def::Option(_), _), (Def::Option(_), _)) => {
266 let from_option = from.into_option().unwrap();
267 let to_option = to.into_option().unwrap();
268
269 let (Some(from_value), Some(to_value)) = (from_option.value(), to_option.value())
270 else {
271 return Diff::Replace { from, to };
272 };
273
274 let updates = sequences::diff_with_options(vec![from_value], vec![to_value], options);
276
277 if updates.is_empty() {
278 return Diff::Equal { value: Some(from) };
279 }
280
281 Diff::User {
282 from: from.shape(),
283 to: to.shape(),
284 variant: Some("Some"),
285 value: Value::Tuple { updates },
286 }
287 }
288 (
289 (Def::List(_) | Def::Slice(_), _) | (_, Type::Sequence(_)),
290 (Def::List(_) | Def::Slice(_), _) | (_, Type::Sequence(_)),
291 ) => {
292 let from_list = from.into_list_like().unwrap();
293 let to_list = to.into_list_like().unwrap();
294
295 let updates = sequences::diff_with_options(
296 from_list.iter().collect::<Vec<_>>(),
297 to_list.iter().collect::<Vec<_>>(),
298 options,
299 );
300
301 if updates.is_empty() {
302 return Diff::Equal { value: Some(from) };
303 }
304
305 Diff::Sequence {
306 from: from.shape(),
307 to: to.shape(),
308 updates,
309 }
310 }
311 ((Def::Map(_), _), (Def::Map(_), _)) => {
312 let from_map = from.into_map().unwrap();
313 let to_map = to.into_map().unwrap();
314
315 let mut updates = HashMap::new();
316 let mut deletions = HashMap::new();
317 let mut insertions = HashMap::new();
318 let mut unchanged = HashSet::new();
319
320 let mut from_entries: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
322 for (key, value) in from_map.iter() {
323 let key_str = format!("{:?}", key);
324 from_entries.insert(key_str, value);
325 }
326
327 let mut to_entries: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
329 for (key, value) in to_map.iter() {
330 let key_str = format!("{:?}", key);
331 to_entries.insert(key_str, value);
332 }
333
334 for (key, from_value) in &from_entries {
336 if let Some(to_value) = to_entries.get(key) {
337 let diff = diff_new_peek_with_options(*from_value, *to_value, options);
338 if diff.is_equal() {
339 unchanged.insert(Cow::Owned(key.clone()));
340 } else {
341 updates.insert(Cow::Owned(key.clone()), diff);
342 }
343 } else {
344 deletions.insert(Cow::Owned(key.clone()), *from_value);
345 }
346 }
347
348 for (key, to_value) in &to_entries {
349 if !from_entries.contains_key(key) {
350 insertions.insert(Cow::Owned(key.clone()), *to_value);
351 }
352 }
353
354 let is_empty = updates.is_empty() && deletions.is_empty() && insertions.is_empty();
355 if is_empty {
356 return Diff::Equal { value: Some(from) };
357 }
358
359 Diff::User {
360 from: from.shape(),
361 to: to.shape(),
362 variant: None,
363 value: Value::Struct {
364 updates,
365 deletions,
366 insertions,
367 unchanged,
368 },
369 }
370 }
371 ((Def::Set(_), _), (Def::Set(_), _)) => {
372 let from_set = from.into_set().unwrap();
373 let to_set = to.into_set().unwrap();
374
375 let mut from_items: HashSet<String> = HashSet::new();
377 for item in from_set.iter() {
378 from_items.insert(format!("{:?}", item));
379 }
380
381 let mut to_items: HashSet<String> = HashSet::new();
382 for item in to_set.iter() {
383 to_items.insert(format!("{:?}", item));
384 }
385
386 if from_items == to_items {
388 return Diff::Equal { value: Some(from) };
389 }
390
391 Diff::Replace { from, to }
392 }
393 ((Def::DynamicValue(_), _), (Def::DynamicValue(_), _)) => {
394 diff_dynamic_values(from, to, options)
395 }
396 ((Def::DynamicValue(_), _), _) => diff_dynamic_vs_concrete(from, to, false, options),
398 (_, (Def::DynamicValue(_), _)) => diff_dynamic_vs_concrete(to, from, true, options),
399 _ => Diff::Replace { from, to },
400 }
401}
402
403pub fn diff_new_peek<'mem, 'facet>(
405 from: Peek<'mem, 'facet>,
406 to: Peek<'mem, 'facet>,
407) -> Diff<'mem, 'facet> {
408 diff_new_peek_with_options(from, to, &DiffOptions::default())
409}
410
411fn diff_dynamic_values<'mem, 'facet>(
413 from: Peek<'mem, 'facet>,
414 to: Peek<'mem, 'facet>,
415 options: &DiffOptions,
416) -> Diff<'mem, 'facet> {
417 let from_dyn = from.into_dynamic_value().unwrap();
418 let to_dyn = to.into_dynamic_value().unwrap();
419
420 let from_kind = from_dyn.kind();
421 let to_kind = to_dyn.kind();
422
423 if from_kind != to_kind {
425 return Diff::Replace { from, to };
426 }
427
428 match from_kind {
429 DynValueKind::Null => Diff::Equal { value: Some(from) },
430 DynValueKind::Bool => {
431 if from_dyn.as_bool() == to_dyn.as_bool() {
432 Diff::Equal { value: Some(from) }
433 } else {
434 Diff::Replace { from, to }
435 }
436 }
437 DynValueKind::Number => {
438 let same = match (from_dyn.as_i64(), to_dyn.as_i64()) {
440 (Some(l), Some(r)) => l == r,
441 _ => match (from_dyn.as_u64(), to_dyn.as_u64()) {
442 (Some(l), Some(r)) => l == r,
443 _ => match (from_dyn.as_f64(), to_dyn.as_f64()) {
444 (Some(l), Some(r)) => l == r,
445 _ => false,
446 },
447 },
448 };
449 if same {
450 Diff::Equal { value: Some(from) }
451 } else {
452 Diff::Replace { from, to }
453 }
454 }
455 DynValueKind::String => {
456 if from_dyn.as_str() == to_dyn.as_str() {
457 Diff::Equal { value: Some(from) }
458 } else {
459 Diff::Replace { from, to }
460 }
461 }
462 DynValueKind::Bytes => {
463 if from_dyn.as_bytes() == to_dyn.as_bytes() {
464 Diff::Equal { value: Some(from) }
465 } else {
466 Diff::Replace { from, to }
467 }
468 }
469 DynValueKind::Array => {
470 let from_iter = from_dyn.array_iter();
472 let to_iter = to_dyn.array_iter();
473
474 let from_elems: Vec<_> = from_iter.map(|i| i.collect()).unwrap_or_default();
475 let to_elems: Vec<_> = to_iter.map(|i| i.collect()).unwrap_or_default();
476
477 let updates = sequences::diff_with_options(from_elems, to_elems, options);
478
479 if updates.is_empty() {
480 return Diff::Equal { value: Some(from) };
481 }
482
483 Diff::Sequence {
484 from: from.shape(),
485 to: to.shape(),
486 updates,
487 }
488 }
489 DynValueKind::Object => {
490 let from_len = from_dyn.object_len().unwrap_or(0);
492 let to_len = to_dyn.object_len().unwrap_or(0);
493
494 let mut updates = HashMap::new();
495 let mut deletions = HashMap::new();
496 let mut insertions = HashMap::new();
497 let mut unchanged = HashSet::new();
498
499 let mut from_keys: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
501 for i in 0..from_len {
502 if let Some((key, value)) = from_dyn.object_get_entry(i) {
503 from_keys.insert(key.to_owned(), value);
504 }
505 }
506
507 let mut to_keys: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
509 for i in 0..to_len {
510 if let Some((key, value)) = to_dyn.object_get_entry(i) {
511 to_keys.insert(key.to_owned(), value);
512 }
513 }
514
515 for (key, from_value) in &from_keys {
517 if let Some(to_value) = to_keys.get(key) {
518 let diff = diff_new_peek_with_options(*from_value, *to_value, options);
519 if diff.is_equal() {
520 unchanged.insert(Cow::Owned(key.clone()));
521 } else {
522 updates.insert(Cow::Owned(key.clone()), diff);
523 }
524 } else {
525 deletions.insert(Cow::Owned(key.clone()), *from_value);
526 }
527 }
528
529 for (key, to_value) in &to_keys {
530 if !from_keys.contains_key(key) {
531 insertions.insert(Cow::Owned(key.clone()), *to_value);
532 }
533 }
534
535 let is_empty = updates.is_empty() && deletions.is_empty() && insertions.is_empty();
536 if is_empty {
537 return Diff::Equal { value: Some(from) };
538 }
539
540 Diff::User {
541 from: from.shape(),
542 to: to.shape(),
543 variant: None,
544 value: Value::Struct {
545 updates,
546 deletions,
547 insertions,
548 unchanged,
549 },
550 }
551 }
552 DynValueKind::DateTime => {
553 if from_dyn.as_datetime() == to_dyn.as_datetime() {
555 Diff::Equal { value: Some(from) }
556 } else {
557 Diff::Replace { from, to }
558 }
559 }
560 DynValueKind::QName | DynValueKind::Uuid => {
561 Diff::Replace { from, to }
564 }
565 }
566}
567
568fn diff_dynamic_vs_concrete<'mem, 'facet>(
572 dyn_peek: Peek<'mem, 'facet>,
573 concrete_peek: Peek<'mem, 'facet>,
574 swapped: bool,
575 options: &DiffOptions,
576) -> Diff<'mem, 'facet> {
577 let (from_peek, to_peek) = if swapped {
579 (concrete_peek, dyn_peek)
580 } else {
581 (dyn_peek, concrete_peek)
582 };
583 let dyn_val = dyn_peek.into_dynamic_value().unwrap();
584 let dyn_kind = dyn_val.kind();
585
586 match dyn_kind {
588 DynValueKind::Bool => {
589 if concrete_peek
590 .get::<bool>()
591 .ok()
592 .is_some_and(|&v| dyn_val.as_bool() == Some(v))
593 {
594 return Diff::Equal {
595 value: Some(from_peek),
596 };
597 }
598 }
599 DynValueKind::Number => {
600 let is_equal =
601 concrete_peek.get::<i8>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
603 || concrete_peek.get::<i16>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
604 || concrete_peek.get::<i32>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
605 || concrete_peek.get::<i64>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v))
606 || concrete_peek.get::<isize>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
607 || concrete_peek.get::<u8>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
609 || concrete_peek.get::<u16>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
610 || concrete_peek.get::<u32>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
611 || concrete_peek.get::<u64>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v))
612 || concrete_peek.get::<usize>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
613 || concrete_peek.get::<f32>().ok().is_some_and(|&v| dyn_val.as_f64() == Some(v as f64))
615 || concrete_peek.get::<f64>().ok().is_some_and(|&v| dyn_val.as_f64() == Some(v));
616 if is_equal {
617 return Diff::Equal {
618 value: Some(from_peek),
619 };
620 }
621 }
622 DynValueKind::String => {
623 if concrete_peek
624 .as_str()
625 .is_some_and(|s| dyn_val.as_str() == Some(s))
626 {
627 return Diff::Equal {
628 value: Some(from_peek),
629 };
630 }
631 }
632 DynValueKind::Array => {
633 if let Ok(concrete_list) = concrete_peek.into_list_like() {
635 let dyn_elems: Vec<_> = dyn_val
636 .array_iter()
637 .map(|i| i.collect())
638 .unwrap_or_default();
639 let concrete_elems: Vec<_> = concrete_list.iter().collect();
640
641 let (from_elems, to_elems) = if swapped {
643 (concrete_elems, dyn_elems)
644 } else {
645 (dyn_elems, concrete_elems)
646 };
647 let updates = sequences::diff_with_options(from_elems, to_elems, options);
648
649 if updates.is_empty() {
650 return Diff::Equal {
651 value: Some(from_peek),
652 };
653 }
654
655 return Diff::Sequence {
656 from: from_peek.shape(),
657 to: to_peek.shape(),
658 updates,
659 };
660 }
661 }
662 DynValueKind::Object => {
663 if let Ok(concrete_struct) = concrete_peek.into_struct() {
665 let dyn_len = dyn_val.object_len().unwrap_or(0);
666
667 let mut updates = HashMap::new();
668 let mut deletions = HashMap::new();
669 let mut insertions = HashMap::new();
670 let mut unchanged = HashSet::new();
671
672 let mut dyn_keys: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
674 for i in 0..dyn_len {
675 if let Some((key, value)) = dyn_val.object_get_entry(i) {
676 dyn_keys.insert(key.to_owned(), value);
677 }
678 }
679
680 for (key, dyn_value) in &dyn_keys {
683 if let Ok(concrete_value) = concrete_struct.field_by_name(key) {
684 let diff = if swapped {
685 diff_new_peek_with_options(concrete_value, *dyn_value, options)
686 } else {
687 diff_new_peek_with_options(*dyn_value, concrete_value, options)
688 };
689 if diff.is_equal() {
690 unchanged.insert(Cow::Owned(key.clone()));
691 } else {
692 updates.insert(Cow::Owned(key.clone()), diff);
693 }
694 } else {
695 if swapped {
699 insertions.insert(Cow::Owned(key.clone()), *dyn_value);
700 } else {
701 deletions.insert(Cow::Owned(key.clone()), *dyn_value);
702 }
703 }
704 }
705
706 for (field, concrete_value) in concrete_struct.fields() {
707 if !dyn_keys.contains_key(field.name) {
708 if swapped {
712 deletions.insert(Cow::Borrowed(field.name), concrete_value);
713 } else {
714 insertions.insert(Cow::Borrowed(field.name), concrete_value);
715 }
716 }
717 }
718
719 let is_empty = updates.is_empty() && deletions.is_empty() && insertions.is_empty();
720 if is_empty {
721 return Diff::Equal {
722 value: Some(from_peek),
723 };
724 }
725
726 return Diff::User {
727 from: from_peek.shape(),
728 to: to_peek.shape(),
729 variant: None,
730 value: Value::Struct {
731 updates,
732 deletions,
733 insertions,
734 unchanged,
735 },
736 };
737 }
738 }
739 _ => {}
741 }
742
743 Diff::Replace {
744 from: from_peek,
745 to: to_peek,
746 }
747}
748
749fn try_extract_float(peek: Peek) -> Option<f64> {
751 match peek.scalar_type()? {
752 ScalarType::F64 => Some(*peek.get::<f64>().ok()?),
753 ScalarType::F32 => Some(*peek.get::<f32>().ok()? as f64),
754 _ => None,
755 }
756}
757
758fn check_float_tolerance(from: Peek, to: Peek, tolerance: f64) -> bool {
760 match (try_extract_float(from), try_extract_float(to)) {
761 (Some(f1), Some(f2)) => (f1 - f2).abs() <= tolerance,
762 _ => false,
763 }
764}
765
766fn deref_if_pointer<'mem, 'facet>(peek: Peek<'mem, 'facet>) -> Peek<'mem, 'facet> {
768 if let Ok(ptr) = peek.into_pointer()
769 && let Some(target) = ptr.borrow_inner()
770 {
771 return deref_if_pointer(target);
772 }
773 peek
774}
775
776pub fn collect_leaf_changes<'mem, 'facet>(
783 diff: &Diff<'mem, 'facet>,
784) -> Vec<LeafChange<'mem, 'facet>> {
785 let mut changes = Vec::new();
786 collect_leaf_changes_inner(diff, Path::new(), &mut changes);
787 changes
788}
789
790fn collect_leaf_changes_inner<'mem, 'facet>(
791 diff: &Diff<'mem, 'facet>,
792 path: Path,
793 changes: &mut Vec<LeafChange<'mem, 'facet>>,
794) {
795 match diff {
796 Diff::Equal { .. } => {
797 }
799 Diff::Replace { from, to } => {
800 changes.push(LeafChange {
802 path,
803 kind: LeafChangeKind::Replace {
804 from: *from,
805 to: *to,
806 },
807 });
808 }
809 Diff::User {
810 value,
811 variant,
812 from,
813 ..
814 } => {
815 let is_option = matches!(from.def, Def::Option(_));
818
819 let base_path = if let Some(v) = variant {
820 if is_option && *v == "Some" {
821 path } else {
823 path.with(PathSegment::Variant(Cow::Borrowed(*v)))
824 }
825 } else {
826 path
827 };
828
829 match value {
830 Value::Struct {
831 updates,
832 deletions,
833 insertions,
834 ..
835 } => {
836 for (field, diff) in updates {
838 let field_path = base_path.with(PathSegment::Field(field.clone()));
839 collect_leaf_changes_inner(diff, field_path, changes);
840 }
841 for (field, peek) in deletions {
843 let field_path = base_path.with(PathSegment::Field(field.clone()));
844 changes.push(LeafChange {
845 path: field_path,
846 kind: LeafChangeKind::Delete { value: *peek },
847 });
848 }
849 for (field, peek) in insertions {
851 let field_path = base_path.with(PathSegment::Field(field.clone()));
852 changes.push(LeafChange {
853 path: field_path,
854 kind: LeafChangeKind::Insert { value: *peek },
855 });
856 }
857 }
858 Value::Tuple { updates } => {
859 if is_option {
861 collect_from_updates_for_single_elem(&base_path, updates, changes);
863 } else {
864 collect_from_updates(&base_path, updates, changes);
865 }
866 }
867 }
868 }
869 Diff::Sequence { updates, .. } => {
870 collect_from_updates(&path, updates, changes);
871 }
872 }
873}
874
875fn collect_from_updates_for_single_elem<'mem, 'facet>(
878 base_path: &Path,
879 updates: &Updates<'mem, 'facet>,
880 changes: &mut Vec<LeafChange<'mem, 'facet>>,
881) {
882 if let Some(update_group) = &updates.0.first {
885 if let Some(replace) = &update_group.0.first
887 && replace.removals.len() == 1
888 && replace.additions.len() == 1
889 {
890 let from = replace.removals[0];
891 let to = replace.additions[0];
892 let nested = diff_new_peek(from, to);
893 if matches!(nested, Diff::Replace { .. }) {
894 changes.push(LeafChange {
895 path: base_path.clone(),
896 kind: LeafChangeKind::Replace { from, to },
897 });
898 } else {
899 collect_leaf_changes_inner(&nested, base_path.clone(), changes);
900 }
901 return;
902 }
903 if let Some(diffs) = &update_group.0.last {
905 for diff in diffs {
906 collect_leaf_changes_inner(diff, base_path.clone(), changes);
907 }
908 return;
909 }
910 }
911 collect_from_updates(base_path, updates, changes);
913}
914
915fn collect_from_updates<'mem, 'facet>(
916 base_path: &Path,
917 updates: &Updates<'mem, 'facet>,
918 changes: &mut Vec<LeafChange<'mem, 'facet>>,
919) {
920 let mut index = 0;
922
923 if let Some(update_group) = &updates.0.first {
925 collect_from_update_group(base_path, update_group, &mut index, changes);
926 }
927
928 for (unchanged, update_group) in &updates.0.values {
930 index += unchanged.len();
931 collect_from_update_group(base_path, update_group, &mut index, changes);
932 }
933
934 }
936
937fn collect_from_update_group<'mem, 'facet>(
938 base_path: &Path,
939 group: &crate::UpdatesGroup<'mem, 'facet>,
940 index: &mut usize,
941 changes: &mut Vec<LeafChange<'mem, 'facet>>,
942) {
943 if let Some(replace) = &group.0.first {
945 collect_from_replace_group(base_path, replace, index, changes);
946 }
947
948 for (diffs, replace) in &group.0.values {
950 for diff in diffs {
951 let elem_path = base_path.with(PathSegment::Index(*index));
952 collect_leaf_changes_inner(diff, elem_path, changes);
953 *index += 1;
954 }
955 collect_from_replace_group(base_path, replace, index, changes);
956 }
957
958 if let Some(diffs) = &group.0.last {
960 for diff in diffs {
961 let elem_path = base_path.with(PathSegment::Index(*index));
962 collect_leaf_changes_inner(diff, elem_path, changes);
963 *index += 1;
964 }
965 }
966}
967
968fn collect_from_replace_group<'mem, 'facet>(
969 base_path: &Path,
970 group: &crate::ReplaceGroup<'mem, 'facet>,
971 index: &mut usize,
972 changes: &mut Vec<LeafChange<'mem, 'facet>>,
973) {
974 if group.removals.len() == group.additions.len() {
979 for (from, to) in group.removals.iter().zip(group.additions.iter()) {
981 let elem_path = base_path.with(PathSegment::Index(*index));
982 let nested = diff_new_peek(*from, *to);
984 if matches!(nested, Diff::Replace { .. }) {
985 changes.push(LeafChange {
986 path: elem_path,
987 kind: LeafChangeKind::Replace {
988 from: *from,
989 to: *to,
990 },
991 });
992 } else {
993 collect_leaf_changes_inner(&nested, elem_path, changes);
994 }
995 *index += 1;
996 }
997 } else {
998 for from in &group.removals {
1000 let elem_path = base_path.with(PathSegment::Index(*index));
1001 changes.push(LeafChange {
1002 path: elem_path.clone(),
1003 kind: LeafChangeKind::Delete { value: *from },
1004 });
1005 *index += 1;
1006 }
1007 for to in &group.additions {
1009 let elem_path = base_path.with(PathSegment::Index(*index));
1010 changes.push(LeafChange {
1011 path: elem_path,
1012 kind: LeafChangeKind::Insert { value: *to },
1013 });
1014 *index += 1;
1015 }
1016 }
1017}
1018
1019#[derive(Debug, Clone)]
1021pub struct LeafChange<'mem, 'facet> {
1022 pub path: Path,
1024 pub kind: LeafChangeKind<'mem, 'facet>,
1026}
1027
1028#[derive(Debug, Clone)]
1030pub enum LeafChangeKind<'mem, 'facet> {
1031 Replace {
1033 from: Peek<'mem, 'facet>,
1035 to: Peek<'mem, 'facet>,
1037 },
1038 Delete {
1040 value: Peek<'mem, 'facet>,
1042 },
1043 Insert {
1045 value: Peek<'mem, 'facet>,
1047 },
1048}
1049
1050impl<'mem, 'facet> LeafChange<'mem, 'facet> {
1051 pub fn format_plain(&self) -> String {
1053 use facet_pretty::PrettyPrinter;
1054
1055 let printer = PrettyPrinter::default()
1056 .with_colors(false)
1057 .with_minimal_option_names(true);
1058
1059 let mut out = String::new();
1060
1061 if !self.path.0.is_empty() {
1063 out.push_str(&format!("{}: ", self.path));
1064 }
1065
1066 match &self.kind {
1067 LeafChangeKind::Replace { from, to } => {
1068 out.push_str(&format!(
1069 "{} → {}",
1070 printer.format_peek(*from),
1071 printer.format_peek(*to)
1072 ));
1073 }
1074 LeafChangeKind::Delete { value } => {
1075 out.push_str(&format!("- {}", printer.format_peek(*value)));
1076 }
1077 LeafChangeKind::Insert { value } => {
1078 out.push_str(&format!("+ {}", printer.format_peek(*value)));
1079 }
1080 }
1081
1082 out
1083 }
1084
1085 pub fn format_colored(&self) -> String {
1087 use facet_pretty::{PrettyPrinter, tokyo_night};
1088 use owo_colors::OwoColorize;
1089
1090 let printer = PrettyPrinter::default()
1091 .with_colors(false)
1092 .with_minimal_option_names(true);
1093
1094 let mut out = String::new();
1095
1096 if !self.path.0.is_empty() {
1098 out.push_str(&format!(
1099 "{}: ",
1100 format!("{}", self.path).color(tokyo_night::FIELD_NAME)
1101 ));
1102 }
1103
1104 match &self.kind {
1105 LeafChangeKind::Replace { from, to } => {
1106 out.push_str(&format!(
1107 "{} {} {}",
1108 printer.format_peek(*from).color(tokyo_night::DELETION),
1109 "→".color(tokyo_night::COMMENT),
1110 printer.format_peek(*to).color(tokyo_night::INSERTION)
1111 ));
1112 }
1113 LeafChangeKind::Delete { value } => {
1114 out.push_str(&format!(
1115 "{} {}",
1116 "-".color(tokyo_night::DELETION),
1117 printer.format_peek(*value).color(tokyo_night::DELETION)
1118 ));
1119 }
1120 LeafChangeKind::Insert { value } => {
1121 out.push_str(&format!(
1122 "{} {}",
1123 "+".color(tokyo_night::INSERTION),
1124 printer.format_peek(*value).color(tokyo_night::INSERTION)
1125 ));
1126 }
1127 }
1128
1129 out
1130 }
1131}
1132
1133impl<'mem, 'facet> std::fmt::Display for LeafChange<'mem, 'facet> {
1134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1135 write!(f, "{}", self.format_plain())
1136 }
1137}
1138
1139#[derive(Debug, Clone)]
1141pub struct DiffFormat {
1142 pub colors: bool,
1144 pub max_inline_changes: usize,
1146 pub prefer_compact: bool,
1148}
1149
1150impl Default for DiffFormat {
1151 fn default() -> Self {
1152 Self {
1153 colors: true,
1154 max_inline_changes: 10,
1155 prefer_compact: true,
1156 }
1157 }
1158}
1159
1160pub fn format_diff(diff: &Diff<'_, '_>, config: &DiffFormat) -> String {
1165 if matches!(diff, Diff::Equal { .. }) {
1166 return if config.colors {
1167 use facet_pretty::tokyo_night;
1168 use owo_colors::OwoColorize;
1169 "(no changes)".color(tokyo_night::MUTED).to_string()
1170 } else {
1171 "(no changes)".to_string()
1172 };
1173 }
1174
1175 let changes = collect_leaf_changes(diff);
1176
1177 if changes.is_empty() {
1178 return if config.colors {
1179 use facet_pretty::tokyo_night;
1180 use owo_colors::OwoColorize;
1181 "(no changes)".color(tokyo_night::MUTED).to_string()
1182 } else {
1183 "(no changes)".to_string()
1184 };
1185 }
1186
1187 if config.prefer_compact && changes.len() <= config.max_inline_changes {
1189 let mut out = String::new();
1190 for (i, change) in changes.iter().enumerate() {
1191 if i > 0 {
1192 out.push('\n');
1193 }
1194 if config.colors {
1195 out.push_str(&change.format_colored());
1196 } else {
1197 out.push_str(&change.format_plain());
1198 }
1199 }
1200 return out;
1201 }
1202
1203 if changes.len() > config.max_inline_changes {
1205 let mut out = String::new();
1206
1207 for (i, change) in changes.iter().take(config.max_inline_changes).enumerate() {
1209 if i > 0 {
1210 out.push('\n');
1211 }
1212 if config.colors {
1213 out.push_str(&change.format_colored());
1214 } else {
1215 out.push_str(&change.format_plain());
1216 }
1217 }
1218
1219 let remaining = changes.len() - config.max_inline_changes;
1221 if remaining > 0 {
1222 out.push('\n');
1223 let summary = format!(
1224 "... and {} more change{}",
1225 remaining,
1226 if remaining == 1 { "" } else { "s" }
1227 );
1228 if config.colors {
1229 use facet_pretty::tokyo_night;
1230 use owo_colors::OwoColorize;
1231 out.push_str(&summary.color(tokyo_night::MUTED).to_string());
1232 } else {
1233 out.push_str(&summary);
1234 }
1235 }
1236 return out;
1237 }
1238
1239 format!("{diff}")
1241}
1242
1243pub fn format_diff_default(diff: &Diff<'_, '_>) -> String {
1245 format_diff(diff, &DiffFormat::default())
1246}
1247
1248pub fn format_diff_compact(diff: &Diff<'_, '_>) -> String {
1250 format_diff(
1251 diff,
1252 &DiffFormat {
1253 prefer_compact: true,
1254 max_inline_changes: usize::MAX,
1255 ..Default::default()
1256 },
1257 )
1258}
1259
1260pub fn format_diff_compact_plain(diff: &Diff<'_, '_>) -> String {
1262 format_diff(
1263 diff,
1264 &DiffFormat {
1265 colors: false,
1266 prefer_compact: true,
1267 max_inline_changes: usize::MAX,
1268 },
1269 )
1270}