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 const 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 = if field.proxy.is_some() {
124 match (
125 from.custom_serialization(field),
126 to.custom_serialization(field),
127 ) {
128 (Ok(from_proxy), Ok(to_proxy)) => {
129 let proxy_diff = diff_new_peek_with_options(
130 from_proxy.as_peek(),
131 to_proxy.as_peek(),
132 options,
133 );
134 if proxy_diff.is_equal() {
136 Diff::Equal { value: Some(from) }
137 } else {
138 Diff::Replace { from, to }
139 }
140 }
141 _ => diff_new_peek_with_options(from, to, options),
143 }
144 } else {
145 diff_new_peek_with_options(from, to, options)
146 };
147 if diff.is_equal() {
148 unchanged.insert(Cow::Borrowed(field.name));
149 } else {
150 updates.insert(Cow::Borrowed(field.name), diff);
151 }
152 } else {
153 deletions.insert(Cow::Borrowed(field.name), from);
154 }
155 }
156
157 for (field, to) in to_ty.fields() {
158 if from_ty.field_by_name(field.name).is_err() {
159 insertions.insert(Cow::Borrowed(field.name), to);
160 }
161 }
162 Value::Struct {
163 updates,
164 deletions,
165 insertions,
166 unchanged,
167 }
168 };
169
170 let is_empty = match &value {
172 Value::Tuple { updates } => updates.is_empty(),
173 Value::Struct {
174 updates,
175 deletions,
176 insertions,
177 ..
178 } => updates.is_empty() && deletions.is_empty() && insertions.is_empty(),
179 };
180 if is_empty {
181 return Diff::Equal { value: Some(from) };
182 }
183
184 Diff::User {
185 from: from.shape(),
186 to: to.shape(),
187 variant: None,
188 value,
189 }
190 }
191 ((_, Type::User(UserType::Enum(_))), (_, Type::User(UserType::Enum(_)))) => {
192 let from_enum = from.into_enum().unwrap();
193 let to_enum = to.into_enum().unwrap();
194
195 let from_variant = from_enum.active_variant().unwrap();
196 let to_variant = to_enum.active_variant().unwrap();
197
198 if from_variant.name != to_variant.name
199 || from_variant.data.kind != to_variant.data.kind
200 {
201 return Diff::Replace { from, to };
202 }
203
204 let value =
205 if [StructKind::Tuple, StructKind::TupleStruct].contains(&from_variant.data.kind) {
206 let from = from_enum.fields().map(|x| x.1).collect();
207 let to = to_enum.fields().map(|x| x.1).collect();
208
209 let updates = sequences::diff_with_options(from, to, options);
210
211 Value::Tuple { updates }
212 } else {
213 let mut updates = HashMap::new();
214 let mut deletions = HashMap::new();
215 let mut insertions = HashMap::new();
216 let mut unchanged = HashSet::new();
217
218 for (field, from) in from_enum.fields() {
219 if let Ok(Some(to)) = to_enum.field_by_name(field.name) {
220 let diff = if field.proxy.is_some() {
225 match (
226 from.custom_serialization(field),
227 to.custom_serialization(field),
228 ) {
229 (Ok(from_proxy), Ok(to_proxy)) => {
230 let proxy_diff = diff_new_peek_with_options(
231 from_proxy.as_peek(),
232 to_proxy.as_peek(),
233 options,
234 );
235 if proxy_diff.is_equal() {
237 Diff::Equal { value: Some(from) }
238 } else {
239 Diff::Replace { from, to }
240 }
241 }
242 _ => diff_new_peek_with_options(from, to, options),
244 }
245 } else {
246 diff_new_peek_with_options(from, to, options)
247 };
248 if diff.is_equal() {
249 unchanged.insert(Cow::Borrowed(field.name));
250 } else {
251 updates.insert(Cow::Borrowed(field.name), diff);
252 }
253 } else {
254 deletions.insert(Cow::Borrowed(field.name), from);
255 }
256 }
257
258 for (field, to) in to_enum.fields() {
259 if !from_enum
260 .field_by_name(field.name)
261 .is_ok_and(|x| x.is_some())
262 {
263 insertions.insert(Cow::Borrowed(field.name), to);
264 }
265 }
266
267 Value::Struct {
268 updates,
269 deletions,
270 insertions,
271 unchanged,
272 }
273 };
274
275 let is_empty = match &value {
277 Value::Tuple { updates } => updates.is_empty(),
278 Value::Struct {
279 updates,
280 deletions,
281 insertions,
282 ..
283 } => updates.is_empty() && deletions.is_empty() && insertions.is_empty(),
284 };
285 if is_empty {
286 return Diff::Equal { value: Some(from) };
287 }
288
289 Diff::User {
290 from: from_enum.shape(),
291 to: to_enum.shape(),
292 variant: Some(from_variant.name),
293 value,
294 }
295 }
296 ((Def::Option(_), _), (Def::Option(_), _)) => {
297 let from_option = from.into_option().unwrap();
298 let to_option = to.into_option().unwrap();
299
300 let (Some(from_value), Some(to_value)) = (from_option.value(), to_option.value())
301 else {
302 return Diff::Replace { from, to };
303 };
304
305 let updates = sequences::diff_with_options(vec![from_value], vec![to_value], options);
307
308 if updates.is_empty() {
309 return Diff::Equal { value: Some(from) };
310 }
311
312 Diff::User {
313 from: from.shape(),
314 to: to.shape(),
315 variant: Some("Some"),
316 value: Value::Tuple { updates },
317 }
318 }
319 (
320 (Def::List(_) | Def::Slice(_), _) | (_, Type::Sequence(_)),
321 (Def::List(_) | Def::Slice(_), _) | (_, Type::Sequence(_)),
322 ) => {
323 let from_list = from.into_list_like().unwrap();
324 let to_list = to.into_list_like().unwrap();
325
326 let updates = sequences::diff_with_options(
327 from_list.iter().collect::<Vec<_>>(),
328 to_list.iter().collect::<Vec<_>>(),
329 options,
330 );
331
332 if updates.is_empty() {
333 return Diff::Equal { value: Some(from) };
334 }
335
336 Diff::Sequence {
337 from: from.shape(),
338 to: to.shape(),
339 updates,
340 }
341 }
342 ((Def::Map(_), _), (Def::Map(_), _)) => {
343 let from_map = from.into_map().unwrap();
344 let to_map = to.into_map().unwrap();
345
346 let mut updates = HashMap::new();
347 let mut deletions = HashMap::new();
348 let mut insertions = HashMap::new();
349 let mut unchanged = HashSet::new();
350
351 let mut from_entries: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
353 for (key, value) in from_map.iter() {
354 let key_str = format!("{:?}", key);
355 from_entries.insert(key_str, value);
356 }
357
358 let mut to_entries: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
360 for (key, value) in to_map.iter() {
361 let key_str = format!("{:?}", key);
362 to_entries.insert(key_str, value);
363 }
364
365 for (key, from_value) in &from_entries {
367 if let Some(to_value) = to_entries.get(key) {
368 let diff = diff_new_peek_with_options(*from_value, *to_value, options);
369 if diff.is_equal() {
370 unchanged.insert(Cow::Owned(key.clone()));
371 } else {
372 updates.insert(Cow::Owned(key.clone()), diff);
373 }
374 } else {
375 deletions.insert(Cow::Owned(key.clone()), *from_value);
376 }
377 }
378
379 for (key, to_value) in &to_entries {
380 if !from_entries.contains_key(key) {
381 insertions.insert(Cow::Owned(key.clone()), *to_value);
382 }
383 }
384
385 let is_empty = updates.is_empty() && deletions.is_empty() && insertions.is_empty();
386 if is_empty {
387 return Diff::Equal { value: Some(from) };
388 }
389
390 Diff::User {
391 from: from.shape(),
392 to: to.shape(),
393 variant: None,
394 value: Value::Struct {
395 updates,
396 deletions,
397 insertions,
398 unchanged,
399 },
400 }
401 }
402 ((Def::Set(_), _), (Def::Set(_), _)) => {
403 let from_set = from.into_set().unwrap();
404 let to_set = to.into_set().unwrap();
405
406 let mut from_items: HashSet<String> = HashSet::new();
408 for item in from_set.iter() {
409 from_items.insert(format!("{:?}", item));
410 }
411
412 let mut to_items: HashSet<String> = HashSet::new();
413 for item in to_set.iter() {
414 to_items.insert(format!("{:?}", item));
415 }
416
417 if from_items == to_items {
419 return Diff::Equal { value: Some(from) };
420 }
421
422 Diff::Replace { from, to }
423 }
424 ((Def::DynamicValue(_), _), (Def::DynamicValue(_), _)) => {
425 diff_dynamic_values(from, to, options)
426 }
427 ((Def::DynamicValue(_), _), _) => diff_dynamic_vs_concrete(from, to, false, options),
429 (_, (Def::DynamicValue(_), _)) => diff_dynamic_vs_concrete(to, from, true, options),
430 _ => Diff::Replace { from, to },
431 }
432}
433
434pub fn diff_new_peek<'mem, 'facet>(
436 from: Peek<'mem, 'facet>,
437 to: Peek<'mem, 'facet>,
438) -> Diff<'mem, 'facet> {
439 diff_new_peek_with_options(from, to, &DiffOptions::default())
440}
441
442fn diff_dynamic_values<'mem, 'facet>(
444 from: Peek<'mem, 'facet>,
445 to: Peek<'mem, 'facet>,
446 options: &DiffOptions,
447) -> Diff<'mem, 'facet> {
448 let from_dyn = from.into_dynamic_value().unwrap();
449 let to_dyn = to.into_dynamic_value().unwrap();
450
451 let from_kind = from_dyn.kind();
452 let to_kind = to_dyn.kind();
453
454 if from_kind != to_kind {
456 return Diff::Replace { from, to };
457 }
458
459 match from_kind {
460 DynValueKind::Null => Diff::Equal { value: Some(from) },
461 DynValueKind::Bool => {
462 if from_dyn.as_bool() == to_dyn.as_bool() {
463 Diff::Equal { value: Some(from) }
464 } else {
465 Diff::Replace { from, to }
466 }
467 }
468 DynValueKind::Number => {
469 let same = match (from_dyn.as_i64(), to_dyn.as_i64()) {
471 (Some(l), Some(r)) => l == r,
472 _ => match (from_dyn.as_u64(), to_dyn.as_u64()) {
473 (Some(l), Some(r)) => l == r,
474 _ => match (from_dyn.as_f64(), to_dyn.as_f64()) {
475 (Some(l), Some(r)) => l == r,
476 _ => false,
477 },
478 },
479 };
480 if same {
481 Diff::Equal { value: Some(from) }
482 } else {
483 Diff::Replace { from, to }
484 }
485 }
486 DynValueKind::String => {
487 if from_dyn.as_str() == to_dyn.as_str() {
488 Diff::Equal { value: Some(from) }
489 } else {
490 Diff::Replace { from, to }
491 }
492 }
493 DynValueKind::Bytes => {
494 if from_dyn.as_bytes() == to_dyn.as_bytes() {
495 Diff::Equal { value: Some(from) }
496 } else {
497 Diff::Replace { from, to }
498 }
499 }
500 DynValueKind::Array => {
501 let from_iter = from_dyn.array_iter();
503 let to_iter = to_dyn.array_iter();
504
505 let from_elems: Vec<_> = from_iter.map(|i| i.collect()).unwrap_or_default();
506 let to_elems: Vec<_> = to_iter.map(|i| i.collect()).unwrap_or_default();
507
508 let updates = sequences::diff_with_options(from_elems, to_elems, options);
509
510 if updates.is_empty() {
511 return Diff::Equal { value: Some(from) };
512 }
513
514 Diff::Sequence {
515 from: from.shape(),
516 to: to.shape(),
517 updates,
518 }
519 }
520 DynValueKind::Object => {
521 let from_len = from_dyn.object_len().unwrap_or(0);
523 let to_len = to_dyn.object_len().unwrap_or(0);
524
525 let mut updates = HashMap::new();
526 let mut deletions = HashMap::new();
527 let mut insertions = HashMap::new();
528 let mut unchanged = HashSet::new();
529
530 let mut from_keys: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
532 for i in 0..from_len {
533 if let Some((key, value)) = from_dyn.object_get_entry(i) {
534 from_keys.insert(key.to_owned(), value);
535 }
536 }
537
538 let mut to_keys: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
540 for i in 0..to_len {
541 if let Some((key, value)) = to_dyn.object_get_entry(i) {
542 to_keys.insert(key.to_owned(), value);
543 }
544 }
545
546 for (key, from_value) in &from_keys {
548 if let Some(to_value) = to_keys.get(key) {
549 let diff = diff_new_peek_with_options(*from_value, *to_value, options);
550 if diff.is_equal() {
551 unchanged.insert(Cow::Owned(key.clone()));
552 } else {
553 updates.insert(Cow::Owned(key.clone()), diff);
554 }
555 } else {
556 deletions.insert(Cow::Owned(key.clone()), *from_value);
557 }
558 }
559
560 for (key, to_value) in &to_keys {
561 if !from_keys.contains_key(key) {
562 insertions.insert(Cow::Owned(key.clone()), *to_value);
563 }
564 }
565
566 let is_empty = updates.is_empty() && deletions.is_empty() && insertions.is_empty();
567 if is_empty {
568 return Diff::Equal { value: Some(from) };
569 }
570
571 Diff::User {
572 from: from.shape(),
573 to: to.shape(),
574 variant: None,
575 value: Value::Struct {
576 updates,
577 deletions,
578 insertions,
579 unchanged,
580 },
581 }
582 }
583 DynValueKind::DateTime => {
584 if from_dyn.as_datetime() == to_dyn.as_datetime() {
586 Diff::Equal { value: Some(from) }
587 } else {
588 Diff::Replace { from, to }
589 }
590 }
591 DynValueKind::QName | DynValueKind::Uuid => {
592 Diff::Replace { from, to }
595 }
596 }
597}
598
599fn diff_dynamic_vs_concrete<'mem, 'facet>(
603 dyn_peek: Peek<'mem, 'facet>,
604 concrete_peek: Peek<'mem, 'facet>,
605 swapped: bool,
606 options: &DiffOptions,
607) -> Diff<'mem, 'facet> {
608 let (from_peek, to_peek) = if swapped {
610 (concrete_peek, dyn_peek)
611 } else {
612 (dyn_peek, concrete_peek)
613 };
614 let dyn_val = dyn_peek.into_dynamic_value().unwrap();
615 let dyn_kind = dyn_val.kind();
616
617 match dyn_kind {
619 DynValueKind::Bool => {
620 if concrete_peek
621 .get::<bool>()
622 .ok()
623 .is_some_and(|&v| dyn_val.as_bool() == Some(v))
624 {
625 return Diff::Equal {
626 value: Some(from_peek),
627 };
628 }
629 }
630 DynValueKind::Number => {
631 let is_equal =
632 concrete_peek.get::<i8>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
634 || concrete_peek.get::<i16>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
635 || concrete_peek.get::<i32>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
636 || concrete_peek.get::<i64>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v))
637 || concrete_peek.get::<isize>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
638 || concrete_peek.get::<u8>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
640 || concrete_peek.get::<u16>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
641 || concrete_peek.get::<u32>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
642 || concrete_peek.get::<u64>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v))
643 || concrete_peek.get::<usize>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
644 || concrete_peek.get::<f32>().ok().is_some_and(|&v| dyn_val.as_f64() == Some(v as f64))
646 || concrete_peek.get::<f64>().ok().is_some_and(|&v| dyn_val.as_f64() == Some(v));
647 if is_equal {
648 return Diff::Equal {
649 value: Some(from_peek),
650 };
651 }
652 }
653 DynValueKind::String => {
654 if concrete_peek
655 .as_str()
656 .is_some_and(|s| dyn_val.as_str() == Some(s))
657 {
658 return Diff::Equal {
659 value: Some(from_peek),
660 };
661 }
662 }
663 DynValueKind::Array => {
664 if let Ok(concrete_list) = concrete_peek.into_list_like() {
666 let dyn_elems: Vec<_> = dyn_val
667 .array_iter()
668 .map(|i| i.collect())
669 .unwrap_or_default();
670 let concrete_elems: Vec<_> = concrete_list.iter().collect();
671
672 let (from_elems, to_elems) = if swapped {
674 (concrete_elems, dyn_elems)
675 } else {
676 (dyn_elems, concrete_elems)
677 };
678 let updates = sequences::diff_with_options(from_elems, to_elems, options);
679
680 if updates.is_empty() {
681 return Diff::Equal {
682 value: Some(from_peek),
683 };
684 }
685
686 return Diff::Sequence {
687 from: from_peek.shape(),
688 to: to_peek.shape(),
689 updates,
690 };
691 }
692 }
693 DynValueKind::Object => {
694 if let Ok(concrete_struct) = concrete_peek.into_struct() {
696 let dyn_len = dyn_val.object_len().unwrap_or(0);
697
698 let mut updates = HashMap::new();
699 let mut deletions = HashMap::new();
700 let mut insertions = HashMap::new();
701 let mut unchanged = HashSet::new();
702
703 let mut dyn_keys: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
705 for i in 0..dyn_len {
706 if let Some((key, value)) = dyn_val.object_get_entry(i) {
707 dyn_keys.insert(key.to_owned(), value);
708 }
709 }
710
711 for (key, dyn_value) in &dyn_keys {
714 if let Ok(concrete_value) = concrete_struct.field_by_name(key) {
715 let diff = if swapped {
716 diff_new_peek_with_options(concrete_value, *dyn_value, options)
717 } else {
718 diff_new_peek_with_options(*dyn_value, concrete_value, options)
719 };
720 if diff.is_equal() {
721 unchanged.insert(Cow::Owned(key.clone()));
722 } else {
723 updates.insert(Cow::Owned(key.clone()), diff);
724 }
725 } else {
726 if swapped {
730 insertions.insert(Cow::Owned(key.clone()), *dyn_value);
731 } else {
732 deletions.insert(Cow::Owned(key.clone()), *dyn_value);
733 }
734 }
735 }
736
737 for (field, concrete_value) in concrete_struct.fields() {
738 if !dyn_keys.contains_key(field.name) {
739 if swapped {
743 deletions.insert(Cow::Borrowed(field.name), concrete_value);
744 } else {
745 insertions.insert(Cow::Borrowed(field.name), concrete_value);
746 }
747 }
748 }
749
750 let is_empty = updates.is_empty() && deletions.is_empty() && insertions.is_empty();
751 if is_empty {
752 return Diff::Equal {
753 value: Some(from_peek),
754 };
755 }
756
757 return Diff::User {
758 from: from_peek.shape(),
759 to: to_peek.shape(),
760 variant: None,
761 value: Value::Struct {
762 updates,
763 deletions,
764 insertions,
765 unchanged,
766 },
767 };
768 }
769 }
770 _ => {}
772 }
773
774 Diff::Replace {
775 from: from_peek,
776 to: to_peek,
777 }
778}
779
780fn try_extract_float(peek: Peek) -> Option<f64> {
782 match peek.scalar_type()? {
783 ScalarType::F64 => Some(*peek.get::<f64>().ok()?),
784 ScalarType::F32 => Some(*peek.get::<f32>().ok()? as f64),
785 _ => None,
786 }
787}
788
789fn check_float_tolerance(from: Peek, to: Peek, tolerance: f64) -> bool {
791 match (try_extract_float(from), try_extract_float(to)) {
792 (Some(f1), Some(f2)) => (f1 - f2).abs() <= tolerance,
793 _ => false,
794 }
795}
796
797fn deref_if_pointer<'mem, 'facet>(peek: Peek<'mem, 'facet>) -> Peek<'mem, 'facet> {
799 if let Ok(ptr) = peek.into_pointer()
800 && let Some(target) = ptr.borrow_inner()
801 {
802 return deref_if_pointer(target);
803 }
804 peek
805}
806
807pub fn collect_leaf_changes<'mem, 'facet>(
814 diff: &Diff<'mem, 'facet>,
815) -> Vec<LeafChange<'mem, 'facet>> {
816 let mut changes = Vec::new();
817 collect_leaf_changes_inner(diff, Path::new(), &mut changes);
818 changes
819}
820
821fn collect_leaf_changes_inner<'mem, 'facet>(
822 diff: &Diff<'mem, 'facet>,
823 path: Path,
824 changes: &mut Vec<LeafChange<'mem, 'facet>>,
825) {
826 match diff {
827 Diff::Equal { .. } => {
828 }
830 Diff::Replace { from, to } => {
831 changes.push(LeafChange {
833 path,
834 kind: LeafChangeKind::Replace {
835 from: *from,
836 to: *to,
837 },
838 });
839 }
840 Diff::User {
841 value,
842 variant,
843 from,
844 ..
845 } => {
846 let is_option = matches!(from.def, Def::Option(_));
849
850 let base_path = if let Some(v) = variant {
851 if is_option && *v == "Some" {
852 path } else {
854 path.with(PathSegment::Variant(Cow::Borrowed(*v)))
855 }
856 } else {
857 path
858 };
859
860 match value {
861 Value::Struct {
862 updates,
863 deletions,
864 insertions,
865 ..
866 } => {
867 for (field, diff) in updates {
869 let field_path = base_path.with(PathSegment::Field(field.clone()));
870 collect_leaf_changes_inner(diff, field_path, changes);
871 }
872 for (field, peek) in deletions {
874 let field_path = base_path.with(PathSegment::Field(field.clone()));
875 changes.push(LeafChange {
876 path: field_path,
877 kind: LeafChangeKind::Delete { value: *peek },
878 });
879 }
880 for (field, peek) in insertions {
882 let field_path = base_path.with(PathSegment::Field(field.clone()));
883 changes.push(LeafChange {
884 path: field_path,
885 kind: LeafChangeKind::Insert { value: *peek },
886 });
887 }
888 }
889 Value::Tuple { updates } => {
890 if is_option {
892 collect_from_updates_for_single_elem(&base_path, updates, changes);
894 } else {
895 collect_from_updates(&base_path, updates, changes);
896 }
897 }
898 }
899 }
900 Diff::Sequence { updates, .. } => {
901 collect_from_updates(&path, updates, changes);
902 }
903 }
904}
905
906fn collect_from_updates_for_single_elem<'mem, 'facet>(
909 base_path: &Path,
910 updates: &Updates<'mem, 'facet>,
911 changes: &mut Vec<LeafChange<'mem, 'facet>>,
912) {
913 if let Some(update_group) = &updates.0.first {
916 if let Some(replace) = &update_group.0.first
918 && replace.removals.len() == 1
919 && replace.additions.len() == 1
920 {
921 let from = replace.removals[0];
922 let to = replace.additions[0];
923 let nested = diff_new_peek(from, to);
924 if matches!(nested, Diff::Replace { .. }) {
925 changes.push(LeafChange {
926 path: base_path.clone(),
927 kind: LeafChangeKind::Replace { from, to },
928 });
929 } else {
930 collect_leaf_changes_inner(&nested, base_path.clone(), changes);
931 }
932 return;
933 }
934 if let Some(diffs) = &update_group.0.last {
936 for diff in diffs {
937 collect_leaf_changes_inner(diff, base_path.clone(), changes);
938 }
939 return;
940 }
941 }
942 collect_from_updates(base_path, updates, changes);
944}
945
946fn collect_from_updates<'mem, 'facet>(
947 base_path: &Path,
948 updates: &Updates<'mem, 'facet>,
949 changes: &mut Vec<LeafChange<'mem, 'facet>>,
950) {
951 let mut index = 0;
953
954 if let Some(update_group) = &updates.0.first {
956 collect_from_update_group(base_path, update_group, &mut index, changes);
957 }
958
959 for (unchanged, update_group) in &updates.0.values {
961 index += unchanged.len();
962 collect_from_update_group(base_path, update_group, &mut index, changes);
963 }
964
965 }
967
968fn collect_from_update_group<'mem, 'facet>(
969 base_path: &Path,
970 group: &crate::UpdatesGroup<'mem, 'facet>,
971 index: &mut usize,
972 changes: &mut Vec<LeafChange<'mem, 'facet>>,
973) {
974 if let Some(replace) = &group.0.first {
976 collect_from_replace_group(base_path, replace, index, changes);
977 }
978
979 for (diffs, replace) in &group.0.values {
981 for diff in diffs {
982 let elem_path = base_path.with(PathSegment::Index(*index));
983 collect_leaf_changes_inner(diff, elem_path, changes);
984 *index += 1;
985 }
986 collect_from_replace_group(base_path, replace, index, changes);
987 }
988
989 if let Some(diffs) = &group.0.last {
991 for diff in diffs {
992 let elem_path = base_path.with(PathSegment::Index(*index));
993 collect_leaf_changes_inner(diff, elem_path, changes);
994 *index += 1;
995 }
996 }
997}
998
999fn collect_from_replace_group<'mem, 'facet>(
1000 base_path: &Path,
1001 group: &crate::ReplaceGroup<'mem, 'facet>,
1002 index: &mut usize,
1003 changes: &mut Vec<LeafChange<'mem, 'facet>>,
1004) {
1005 if group.removals.len() == group.additions.len() {
1010 for (from, to) in group.removals.iter().zip(group.additions.iter()) {
1012 let elem_path = base_path.with(PathSegment::Index(*index));
1013 let nested = diff_new_peek(*from, *to);
1015 if matches!(nested, Diff::Replace { .. }) {
1016 changes.push(LeafChange {
1017 path: elem_path,
1018 kind: LeafChangeKind::Replace {
1019 from: *from,
1020 to: *to,
1021 },
1022 });
1023 } else {
1024 collect_leaf_changes_inner(&nested, elem_path, changes);
1025 }
1026 *index += 1;
1027 }
1028 } else {
1029 for from in &group.removals {
1031 let elem_path = base_path.with(PathSegment::Index(*index));
1032 changes.push(LeafChange {
1033 path: elem_path.clone(),
1034 kind: LeafChangeKind::Delete { value: *from },
1035 });
1036 *index += 1;
1037 }
1038 for to in &group.additions {
1040 let elem_path = base_path.with(PathSegment::Index(*index));
1041 changes.push(LeafChange {
1042 path: elem_path,
1043 kind: LeafChangeKind::Insert { value: *to },
1044 });
1045 *index += 1;
1046 }
1047 }
1048}
1049
1050#[derive(Debug, Clone)]
1052pub struct LeafChange<'mem, 'facet> {
1053 pub path: Path,
1055 pub kind: LeafChangeKind<'mem, 'facet>,
1057}
1058
1059#[derive(Debug, Clone)]
1061pub enum LeafChangeKind<'mem, 'facet> {
1062 Replace {
1064 from: Peek<'mem, 'facet>,
1066 to: Peek<'mem, 'facet>,
1068 },
1069 Delete {
1071 value: Peek<'mem, 'facet>,
1073 },
1074 Insert {
1076 value: Peek<'mem, 'facet>,
1078 },
1079}
1080
1081impl<'mem, 'facet> LeafChange<'mem, 'facet> {
1082 pub fn format_plain(&self) -> String {
1084 use facet_pretty::PrettyPrinter;
1085
1086 let printer = PrettyPrinter::default()
1087 .with_colors(facet_pretty::ColorMode::Never)
1088 .with_minimal_option_names(true);
1089
1090 let mut out = String::new();
1091
1092 if !self.path.0.is_empty() {
1094 out.push_str(&format!("{}: ", self.path));
1095 }
1096
1097 match &self.kind {
1098 LeafChangeKind::Replace { from, to } => {
1099 out.push_str(&format!(
1100 "{} → {}",
1101 printer.format_peek(*from),
1102 printer.format_peek(*to)
1103 ));
1104 }
1105 LeafChangeKind::Delete { value } => {
1106 out.push_str(&format!("- {}", printer.format_peek(*value)));
1107 }
1108 LeafChangeKind::Insert { value } => {
1109 out.push_str(&format!("+ {}", printer.format_peek(*value)));
1110 }
1111 }
1112
1113 out
1114 }
1115
1116 pub fn format_colored(&self) -> String {
1118 use facet_pretty::{PrettyPrinter, tokyo_night};
1119 use owo_colors::OwoColorize;
1120
1121 let printer = PrettyPrinter::default()
1122 .with_colors(facet_pretty::ColorMode::Never)
1123 .with_minimal_option_names(true);
1124
1125 let mut out = String::new();
1126
1127 if !self.path.0.is_empty() {
1129 out.push_str(&format!(
1130 "{}: ",
1131 format!("{}", self.path).color(tokyo_night::FIELD_NAME)
1132 ));
1133 }
1134
1135 match &self.kind {
1136 LeafChangeKind::Replace { from, to } => {
1137 out.push_str(&format!(
1138 "{} {} {}",
1139 printer.format_peek(*from).color(tokyo_night::DELETION),
1140 "→".color(tokyo_night::COMMENT),
1141 printer.format_peek(*to).color(tokyo_night::INSERTION)
1142 ));
1143 }
1144 LeafChangeKind::Delete { value } => {
1145 out.push_str(&format!(
1146 "{} {}",
1147 "-".color(tokyo_night::DELETION),
1148 printer.format_peek(*value).color(tokyo_night::DELETION)
1149 ));
1150 }
1151 LeafChangeKind::Insert { value } => {
1152 out.push_str(&format!(
1153 "{} {}",
1154 "+".color(tokyo_night::INSERTION),
1155 printer.format_peek(*value).color(tokyo_night::INSERTION)
1156 ));
1157 }
1158 }
1159
1160 out
1161 }
1162}
1163
1164impl<'mem, 'facet> std::fmt::Display for LeafChange<'mem, 'facet> {
1165 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1166 write!(f, "{}", self.format_plain())
1167 }
1168}
1169
1170#[derive(Debug, Clone)]
1172pub struct DiffFormat {
1173 pub colors: bool,
1175 pub max_inline_changes: usize,
1177 pub prefer_compact: bool,
1179}
1180
1181impl Default for DiffFormat {
1182 fn default() -> Self {
1183 Self {
1184 colors: true,
1185 max_inline_changes: 10,
1186 prefer_compact: true,
1187 }
1188 }
1189}
1190
1191pub fn format_diff(diff: &Diff<'_, '_>, config: &DiffFormat) -> String {
1196 if matches!(diff, Diff::Equal { .. }) {
1197 return if config.colors {
1198 use facet_pretty::tokyo_night;
1199 use owo_colors::OwoColorize;
1200 "(no changes)".color(tokyo_night::MUTED).to_string()
1201 } else {
1202 "(no changes)".to_string()
1203 };
1204 }
1205
1206 let changes = collect_leaf_changes(diff);
1207
1208 if changes.is_empty() {
1209 return if config.colors {
1210 use facet_pretty::tokyo_night;
1211 use owo_colors::OwoColorize;
1212 "(no changes)".color(tokyo_night::MUTED).to_string()
1213 } else {
1214 "(no changes)".to_string()
1215 };
1216 }
1217
1218 if config.prefer_compact && changes.len() <= config.max_inline_changes {
1220 let mut out = String::new();
1221 for (i, change) in changes.iter().enumerate() {
1222 if i > 0 {
1223 out.push('\n');
1224 }
1225 if config.colors {
1226 out.push_str(&change.format_colored());
1227 } else {
1228 out.push_str(&change.format_plain());
1229 }
1230 }
1231 return out;
1232 }
1233
1234 if changes.len() > config.max_inline_changes {
1236 let mut out = String::new();
1237
1238 for (i, change) in changes.iter().take(config.max_inline_changes).enumerate() {
1240 if i > 0 {
1241 out.push('\n');
1242 }
1243 if config.colors {
1244 out.push_str(&change.format_colored());
1245 } else {
1246 out.push_str(&change.format_plain());
1247 }
1248 }
1249
1250 let remaining = changes.len() - config.max_inline_changes;
1252 if remaining > 0 {
1253 out.push('\n');
1254 let summary = format!(
1255 "... and {} more change{}",
1256 remaining,
1257 if remaining == 1 { "" } else { "s" }
1258 );
1259 if config.colors {
1260 use facet_pretty::tokyo_night;
1261 use owo_colors::OwoColorize;
1262 out.push_str(&summary.color(tokyo_night::MUTED).to_string());
1263 } else {
1264 out.push_str(&summary);
1265 }
1266 }
1267 return out;
1268 }
1269
1270 format!("{diff}")
1272}
1273
1274pub fn format_diff_default(diff: &Diff<'_, '_>) -> String {
1276 format_diff(diff, &DiffFormat::default())
1277}
1278
1279pub fn format_diff_compact(diff: &Diff<'_, '_>) -> String {
1281 format_diff(
1282 diff,
1283 &DiffFormat {
1284 prefer_compact: true,
1285 max_inline_changes: usize::MAX,
1286 ..Default::default()
1287 },
1288 )
1289}
1290
1291pub fn format_diff_compact_plain(diff: &Diff<'_, '_>) -> String {
1293 format_diff(
1294 diff,
1295 &DiffFormat {
1296 colors: false,
1297 prefer_compact: true,
1298 max_inline_changes: usize::MAX,
1299 },
1300 )
1301}