Skip to main content

facet_diff/
diff.rs

1// TODO: Consider using an approach similar to `morph` (bearcove's fork of difftastic)
2// to compute and display the optimal diff path for complex structural changes.
3
4use 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/// Configuration options for diff computation
15#[derive(Debug, Clone, Default)]
16pub struct DiffOptions {
17    /// Tolerance for floating-point comparisons.
18    /// If set, two floats are considered equal if their absolute difference
19    /// is less than or equal to this value.
20    pub float_tolerance: Option<f64>,
21}
22
23impl DiffOptions {
24    /// Create a new `DiffOptions` with default settings.
25    pub fn new() -> Self {
26        Self::default()
27    }
28
29    /// Set the tolerance for floating-point comparisons.
30    pub const fn with_float_tolerance(mut self, tolerance: f64) -> Self {
31        self.float_tolerance = Some(tolerance);
32        self
33    }
34}
35
36/// Extension trait that provides a [`diff`](FacetDiff::diff) method for `Facet` types
37pub trait FacetDiff<'f>: Facet<'f> {
38    /// Computes the difference between two values that implement `Facet`
39    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
48/// Computes the difference between two values that implement `Facet`
49pub 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
56/// Computes the difference between two `Peek` values with options
57pub 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    // Dereference pointers/references to compare the underlying values
63    let from = deref_if_pointer(from);
64    let to = deref_if_pointer(to);
65
66    // Check for equality if both shapes have the same type_identifier and implement PartialEq
67    // This handles cases where shapes are structurally equivalent but have different IDs
68    // (e.g., after deserialization)
69    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    // Check float tolerance if configured
75    let float_equal = options
76        .float_tolerance
77        .map(|tol| check_float_tolerance(from, to, tol))
78        .unwrap_or(false);
79
80    // log::trace!(
81    //     "diff_new_peek: type={} same_type={} from_has_partialeq={} to_has_partialeq={} values_equal={}",
82    //     from.shape().type_identifier,
83    //     same_type,
84    //     from_has_partialeq,
85    //     to_has_partialeq,
86    //     values_equal
87    // );
88
89    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                        // Check for field-level proxy - if present, convert values through
120                        // the proxy before comparing (needed for opaque types).
121                        // Since OwnedPeek has a limited lifetime, we compare proxies for
122                        // equality but return results referencing the original values.
123                        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                                    // Map the proxy diff result back to original values
135                                    if proxy_diff.is_equal() {
136                                        Diff::Equal { value: Some(from) }
137                                    } else {
138                                        Diff::Replace { from, to }
139                                    }
140                                }
141                                // If proxy conversion fails, fall back to direct comparison
142                                _ => 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            // If there are no changes, return Equal instead of User
171            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                            // Check for field-level proxy - if present, convert values through
221                            // the proxy before comparing (needed for opaque types).
222                            // Since OwnedPeek has a limited lifetime, we compare proxies for
223                            // equality but return results referencing the original values.
224                            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                                        // Map the proxy diff result back to original values
236                                        if proxy_diff.is_equal() {
237                                            Diff::Equal { value: Some(from) }
238                                        } else {
239                                            Diff::Replace { from, to }
240                                        }
241                                    }
242                                    // If proxy conversion fails, fall back to direct comparison
243                                    _ => 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            // If there are no changes, return Equal instead of User
276            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            // Use sequences::diff to properly handle nested diffs
306            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            // Collect entries from `from` map with string keys for comparison
352            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            // Collect entries from `to` map
359            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            // Compare entries
366            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            // Collect items from both sets using debug format for comparison
407            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            // Sets are equal if they have the same items
418            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        // DynamicValue vs concrete type
428        ((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
434/// Computes the difference between two `Peek` values (backward compatibility wrapper)
435pub 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
442/// Diff two dynamic values (like `facet_value::Value`)
443fn 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 kinds differ, just return Replace
455    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            // Compare numbers - try exact integer comparison first, then float
470            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            // Use the sequence diff algorithm for arrays
502            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            // Treat objects like struct diffs
522            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            // Collect keys from `from`
531            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            // Collect keys from `to`
539            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            // Compare entries
547            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            // Compare datetime by their components
585            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            // For QName and Uuid, compare by their raw representation
593            // Since they have the same kind, we can only compare by Replace semantics
594            Diff::Replace { from, to }
595        }
596    }
597}
598
599/// Diff a DynamicValue against a concrete type
600/// `dyn_peek` is the DynamicValue, `concrete_peek` is the concrete type
601/// `swapped` indicates if the original from/to were swapped (true means dyn_peek is actually "to")
602fn 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    // Determine actual from/to based on swapped flag
609    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    // Try to match based on the DynamicValue's kind
618    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                // Try signed integers
633                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                // Try unsigned integers
639                || 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                // Try floats
645                || 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            // Try to diff as sequences if the concrete type is list-like
665            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                // Use correct order based on swapped flag
673                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            // Try to diff as struct if the concrete type is a struct
695            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                // Collect keys from dynamic object
704                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                // Compare with concrete struct fields
712                // When swapped, dyn is "to" and concrete is "from", so we need to swap the diff direction
713                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                        // Field in dyn but not in concrete
727                        // If swapped: dyn is "to", so this is an insertion
728                        // If not swapped: dyn is "from", so this is a deletion
729                        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                        // Field in concrete but not in dyn
740                        // If swapped: concrete is "from", so this is a deletion
741                        // If not swapped: concrete is "to", so this is an insertion
742                        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        // For other kinds (Null, Bytes, DateTime), fall through to Replace
771        _ => {}
772    }
773
774    Diff::Replace {
775        from: from_peek,
776        to: to_peek,
777    }
778}
779
780/// Extract a float value from a Peek, handling both f32 and f64
781fn 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
789/// Check if two Peek values are equal within the specified float tolerance
790fn 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
797/// Dereference a pointer/reference to get the underlying value
798fn 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
807/// Collect all leaf-level changes with their paths.
808///
809/// This walks the diff tree recursively and collects every terminal change
810/// (scalar replacements) along with the path to reach them. This is useful
811/// for compact display: if there's only one leaf change deep in a tree,
812/// you can show `path.to.field: old → new` instead of nested structure.
813pub 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            // No change
829        }
830        Diff::Replace { from, to } => {
831            // This is a leaf change
832            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            // For Option::Some, skip the variant in the path since it's implied
847            // (the value exists, so it's Some)
848            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 // Skip "::Some" for options
853                } 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                    // Recurse into field updates
868                    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                    // Deletions are leaf changes
873                    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                    // Insertions are leaf changes
881                    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                    // For single-element tuples (like Option::Some), skip the index
891                    if is_option {
892                        // Recurse directly without adding [0]
893                        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
906/// Special handling for single-element tuples (like Option::Some)
907/// where we want to skip the `[0]` index in the path.
908fn 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    // For single-element tuples, we expect exactly one change
914    // Just use base_path directly instead of adding [0]
915    if let Some(update_group) = &updates.0.first {
916        // Process the first replace group if present
917        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        // Handle nested diffs
935        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    // Fallback: use regular handling
943    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    // Walk through the interspersed structure to collect changes with correct indices
952    let mut index = 0;
953
954    // Process first update group if present
955    if let Some(update_group) = &updates.0.first {
956        collect_from_update_group(base_path, update_group, &mut index, changes);
957    }
958
959    // Process interleaved (unchanged, update) pairs
960    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    // Trailing unchanged items don't add changes
966}
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    // Process first replace group if present
975    if let Some(replace) = &group.0.first {
976        collect_from_replace_group(base_path, replace, index, changes);
977    }
978
979    // Process interleaved (diffs, replace) pairs
980    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    // Process trailing diffs
990    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    // For replace groups, we have removals and additions
1006    // If counts match, treat as 1:1 replacements at the same index
1007    // Otherwise, show as deletions followed by insertions
1008
1009    if group.removals.len() == group.additions.len() {
1010        // 1:1 replacements
1011        for (from, to) in group.removals.iter().zip(group.additions.iter()) {
1012            let elem_path = base_path.with(PathSegment::Index(*index));
1013            // Check if this is actually a nested diff
1014            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        // Mixed deletions and insertions
1030        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        // Insertions happen at current index
1039        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/// A single leaf-level change in a diff, with path information.
1051#[derive(Debug, Clone)]
1052pub struct LeafChange<'mem, 'facet> {
1053    /// The path from root to this change
1054    pub path: Path,
1055    /// The kind of change
1056    pub kind: LeafChangeKind<'mem, 'facet>,
1057}
1058
1059/// The kind of leaf change.
1060#[derive(Debug, Clone)]
1061pub enum LeafChangeKind<'mem, 'facet> {
1062    /// A value was replaced
1063    Replace {
1064        /// The old value
1065        from: Peek<'mem, 'facet>,
1066        /// The new value
1067        to: Peek<'mem, 'facet>,
1068    },
1069    /// A value was deleted
1070    Delete {
1071        /// The deleted value
1072        value: Peek<'mem, 'facet>,
1073    },
1074    /// A value was inserted
1075    Insert {
1076        /// The inserted value
1077        value: Peek<'mem, 'facet>,
1078    },
1079}
1080
1081impl<'mem, 'facet> LeafChange<'mem, 'facet> {
1082    /// Format this change without colors.
1083    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        // Show path if non-empty
1093        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    /// Format this change with colors.
1117    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        // Show path if non-empty (in field name color)
1128        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/// Configuration for diff formatting.
1171#[derive(Debug, Clone)]
1172pub struct DiffFormat {
1173    /// Use colors in output
1174    pub colors: bool,
1175    /// Maximum number of changes before switching to summary mode
1176    pub max_inline_changes: usize,
1177    /// Whether to use compact (path-based) format for few changes
1178    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
1191/// Format the diff with the given configuration.
1192///
1193/// This chooses between compact (path-based) and tree (nested) format
1194/// based on the number of changes and the configuration.
1195pub 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    // Use compact format if preferred and we have few changes
1219    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    // Fall back to tree format for many changes
1235    if changes.len() > config.max_inline_changes {
1236        let mut out = String::new();
1237
1238        // Show first few changes
1239        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        // Show summary of remaining
1251        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    // Default: use Display impl (tree format)
1271    format!("{diff}")
1272}
1273
1274/// Format the diff with default configuration.
1275pub fn format_diff_default(diff: &Diff<'_, '_>) -> String {
1276    format_diff(diff, &DiffFormat::default())
1277}
1278
1279/// Format the diff in compact mode (path-based, no tree structure).
1280pub 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
1291/// Format the diff in compact mode without colors.
1292pub 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}