• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In
Build has been canceled!

facet-rs / facet / 20114574768

10 Dec 2025 09:58PM UTC coverage: 57.703% (+0.002%) from 57.701%
20114574768

push

github

fasterthanlime
Fix: Allow dead_code and remove unused import in sequences.rs

28236 of 48933 relevant lines covered (57.7%)

6362.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

52.17
/facet-diff/src/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

4
use std::borrow::Cow;
5
use std::collections::{HashMap, HashSet};
6

7
use facet::{Def, DynValueKind, StructKind, Type, UserType};
8
use facet_core::Facet;
9
use facet_diff_core::{Diff, Path, PathSegment, Updates, Value};
10
use facet_reflect::{HasFields, Peek, ScalarType};
11

12
use crate::sequences;
13

14
/// Configuration options for diff computation
15
#[derive(Debug, Clone, Default)]
16
pub 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

23
impl DiffOptions {
24
    /// Create a new `DiffOptions` with default settings.
25
    pub fn new() -> Self {
43✔
26
        Self::default()
43✔
27
    }
43✔
28

29
    /// Set the tolerance for floating-point comparisons.
30
    pub fn with_float_tolerance(mut self, tolerance: f64) -> Self {
9✔
31
        self.float_tolerance = Some(tolerance);
9✔
32
        self
9✔
33
    }
9✔
34
}
35

36
/// Extension trait that provides a [`diff`](FacetDiff::diff) method for `Facet` types
37
pub 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

42
impl<'f, T: Facet<'f>> FacetDiff<'f> for T {
43
    fn diff<'a, U: Facet<'f>>(&'a self, other: &'a U) -> Diff<'a, 'f> {
221✔
44
        diff_new(self, other)
221✔
45
    }
221✔
46
}
47

48
/// Computes the difference between two values that implement `Facet`
49
pub fn diff_new<'mem, 'facet, T: Facet<'facet>, U: Facet<'facet>>(
221✔
50
    from: &'mem T,
221✔
51
    to: &'mem U,
221✔
52
) -> Diff<'mem, 'facet> {
221✔
53
    diff_new_peek(Peek::new(from), Peek::new(to))
221✔
54
}
221✔
55

56
/// Computes the difference between two `Peek` values with options
57
pub fn diff_new_peek_with_options<'mem, 'facet>(
574,042✔
58
    from: Peek<'mem, 'facet>,
574,042✔
59
    to: Peek<'mem, 'facet>,
574,042✔
60
    options: &DiffOptions,
574,042✔
61
) -> Diff<'mem, 'facet> {
574,042✔
62
    // Dereference pointers/references to compare the underlying values
63
    let from = deref_if_pointer(from);
574,042✔
64
    let to = deref_if_pointer(to);
574,042✔
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;
574,042✔
70
    let from_has_partialeq = from.shape().is_partial_eq();
574,042✔
71
    let to_has_partialeq = to.shape().is_partial_eq();
574,042✔
72
    let values_equal = from == to;
574,042✔
73

74
    // Check float tolerance if configured
75
    let float_equal = options
574,042✔
76
        .float_tolerance
574,042✔
77
        .map(|tol| check_float_tolerance(from, to, tol))
574,042✔
78
        .unwrap_or(false);
574,042✔
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) {
574,042✔
90
        return Diff::Equal { value: Some(from) };
192,445✔
91
    }
381,597✔
92

93
    match (
94
        (from.shape().def, from.shape().ty),
381,597✔
95
        (to.shape().def, to.shape().ty),
381,597✔
96
    ) {
97
        ((_, Type::User(UserType::Struct(from_ty))), (_, Type::User(UserType::Struct(to_ty))))
189,610✔
98
            if from_ty.kind == to_ty.kind =>
189,610✔
99
        {
100
            let from_ty = from.into_struct().unwrap();
189,610✔
101
            let to_ty = to.into_struct().unwrap();
189,610✔
102

103
            let value = if [StructKind::Tuple, StructKind::TupleStruct].contains(&from_ty.ty().kind)
189,610✔
104
            {
105
                let from = from_ty.fields().map(|x| x.1).collect();
17✔
106
                let to = to_ty.fields().map(|x| x.1).collect();
17✔
107

108
                let updates = sequences::diff_with_options(from, to, options);
17✔
109

110
                Value::Tuple { updates }
17✔
111
            } else {
112
                let mut updates = HashMap::new();
189,593✔
113
                let mut deletions = HashMap::new();
189,593✔
114
                let mut insertions = HashMap::new();
189,593✔
115
                let mut unchanged = HashSet::new();
189,593✔
116

117
                for (field, from) in from_ty.fields() {
382,391✔
118
                    if let Ok(to) = to_ty.field_by_name(field.name) {
382,391✔
119
                        let diff = diff_new_peek_with_options(from, to, options);
382,390✔
120
                        if diff.is_equal() {
382,390✔
121
                            unchanged.insert(Cow::Borrowed(field.name));
381,325✔
122
                        } else {
381,325✔
123
                            updates.insert(Cow::Borrowed(field.name), diff);
1,065✔
124
                        }
1,065✔
125
                    } else {
1✔
126
                        deletions.insert(Cow::Borrowed(field.name), from);
1✔
127
                    }
1✔
128
                }
129

130
                for (field, to) in to_ty.fields() {
382,391✔
131
                    if from_ty.field_by_name(field.name).is_err() {
382,391✔
132
                        insertions.insert(Cow::Borrowed(field.name), to);
1✔
133
                    }
382,390✔
134
                }
135
                Value::Struct {
189,593✔
136
                    updates,
189,593✔
137
                    deletions,
189,593✔
138
                    insertions,
189,593✔
139
                    unchanged,
189,593✔
140
                }
189,593✔
141
            };
142

143
            // If there are no changes, return Equal instead of User
144
            let is_empty = match &value {
189,610✔
145
                Value::Tuple { updates } => updates.is_empty(),
17✔
146
                Value::Struct {
147
                    updates,
189,593✔
148
                    deletions,
189,593✔
149
                    insertions,
189,593✔
150
                    ..
151
                } => updates.is_empty() && deletions.is_empty() && insertions.is_empty(),
189,593✔
152
            };
153
            if is_empty {
189,610✔
154
                return Diff::Equal { value: Some(from) };
188,561✔
155
            }
1,049✔
156

157
            Diff::User {
1,049✔
158
                from: from.shape(),
1,049✔
159
                to: to.shape(),
1,049✔
160
                variant: None,
1,049✔
161
                value,
1,049✔
162
            }
1,049✔
163
        }
164
        ((_, Type::User(UserType::Enum(_))), (_, Type::User(UserType::Enum(_)))) => {
165
            let from_enum = from.into_enum().unwrap();
910✔
166
            let to_enum = to.into_enum().unwrap();
910✔
167

168
            let from_variant = from_enum.active_variant().unwrap();
910✔
169
            let to_variant = to_enum.active_variant().unwrap();
910✔
170

171
            if from_variant.name != to_variant.name
910✔
172
                || from_variant.data.kind != to_variant.data.kind
892✔
173
            {
174
                return Diff::Replace { from, to };
18✔
175
            }
892✔
176

177
            let value =
892✔
178
                if [StructKind::Tuple, StructKind::TupleStruct].contains(&from_variant.data.kind) {
892✔
179
                    let from = from_enum.fields().map(|x| x.1).collect();
598✔
180
                    let to = to_enum.fields().map(|x| x.1).collect();
598✔
181

182
                    let updates = sequences::diff_with_options(from, to, options);
598✔
183

184
                    Value::Tuple { updates }
598✔
185
                } else {
186
                    let mut updates = HashMap::new();
294✔
187
                    let mut deletions = HashMap::new();
294✔
188
                    let mut insertions = HashMap::new();
294✔
189
                    let mut unchanged = HashSet::new();
294✔
190

191
                    for (field, from) in from_enum.fields() {
294✔
192
                        if let Ok(Some(to)) = to_enum.field_by_name(field.name) {
7✔
193
                            let diff = diff_new_peek_with_options(from, to, options);
7✔
194
                            if diff.is_equal() {
7✔
195
                                unchanged.insert(Cow::Borrowed(field.name));
6✔
196
                            } else {
6✔
197
                                updates.insert(Cow::Borrowed(field.name), diff);
1✔
198
                            }
1✔
199
                        } else {
×
200
                            deletions.insert(Cow::Borrowed(field.name), from);
×
201
                        }
×
202
                    }
203

204
                    for (field, to) in to_enum.fields() {
294✔
205
                        if !from_enum
7✔
206
                            .field_by_name(field.name)
7✔
207
                            .is_ok_and(|x| x.is_some())
7✔
208
                        {
×
209
                            insertions.insert(Cow::Borrowed(field.name), to);
×
210
                        }
7✔
211
                    }
212

213
                    Value::Struct {
294✔
214
                        updates,
294✔
215
                        deletions,
294✔
216
                        insertions,
294✔
217
                        unchanged,
294✔
218
                    }
294✔
219
                };
220

221
            // If there are no changes, return Equal instead of User
222
            let is_empty = match &value {
892✔
223
                Value::Tuple { updates } => updates.is_empty(),
598✔
224
                Value::Struct {
225
                    updates,
294✔
226
                    deletions,
294✔
227
                    insertions,
294✔
228
                    ..
229
                } => updates.is_empty() && deletions.is_empty() && insertions.is_empty(),
294✔
230
            };
231
            if is_empty {
892✔
232
                return Diff::Equal { value: Some(from) };
611✔
233
            }
281✔
234

235
            Diff::User {
281✔
236
                from: from_enum.shape(),
281✔
237
                to: to_enum.shape(),
281✔
238
                variant: Some(from_variant.name),
281✔
239
                value,
281✔
240
            }
281✔
241
        }
242
        ((Def::Option(_), _), (Def::Option(_), _)) => {
243
            let from_option = from.into_option().unwrap();
14✔
244
            let to_option = to.into_option().unwrap();
14✔
245

246
            let (Some(from_value), Some(to_value)) = (from_option.value(), to_option.value())
14✔
247
            else {
248
                return Diff::Replace { from, to };
3✔
249
            };
250

251
            // Use sequences::diff to properly handle nested diffs
252
            let updates = sequences::diff_with_options(vec![from_value], vec![to_value], options);
11✔
253

254
            if updates.is_empty() {
11✔
255
                return Diff::Equal { value: Some(from) };
×
256
            }
11✔
257

258
            Diff::User {
11✔
259
                from: from.shape(),
11✔
260
                to: to.shape(),
11✔
261
                variant: Some("Some"),
11✔
262
                value: Value::Tuple { updates },
11✔
263
            }
11✔
264
        }
265
        (
266
            (Def::List(_) | Def::Slice(_), _) | (_, Type::Sequence(_)),
267
            (Def::List(_) | Def::Slice(_), _) | (_, Type::Sequence(_)),
268
        ) => {
269
            let from_list = from.into_list_like().unwrap();
188,758✔
270
            let to_list = to.into_list_like().unwrap();
188,758✔
271

272
            let updates = sequences::diff_with_options(
188,758✔
273
                from_list.iter().collect::<Vec<_>>(),
188,758✔
274
                to_list.iter().collect::<Vec<_>>(),
188,758✔
275
                options,
188,758✔
276
            );
277

278
            if updates.is_empty() {
188,758✔
279
                return Diff::Equal { value: Some(from) };
188,607✔
280
            }
151✔
281

282
            Diff::Sequence {
151✔
283
                from: from.shape(),
151✔
284
                to: to.shape(),
151✔
285
                updates,
151✔
286
            }
151✔
287
        }
288
        ((Def::DynamicValue(_), _), (Def::DynamicValue(_), _)) => {
289
            diff_dynamic_values(from, to, options)
137✔
290
        }
291
        // DynamicValue vs concrete type
292
        ((Def::DynamicValue(_), _), _) => diff_dynamic_vs_concrete(from, to, false, options),
210✔
293
        (_, (Def::DynamicValue(_), _)) => diff_dynamic_vs_concrete(to, from, true, options),
63✔
294
        _ => Diff::Replace { from, to },
1,895✔
295
    }
296
}
574,042✔
297

298
/// Computes the difference between two `Peek` values (backward compatibility wrapper)
299
pub fn diff_new_peek<'mem, 'facet>(
221✔
300
    from: Peek<'mem, 'facet>,
221✔
301
    to: Peek<'mem, 'facet>,
221✔
302
) -> Diff<'mem, 'facet> {
221✔
303
    diff_new_peek_with_options(from, to, &DiffOptions::default())
221✔
304
}
221✔
305

306
/// Diff two dynamic values (like `facet_value::Value`)
307
fn diff_dynamic_values<'mem, 'facet>(
137✔
308
    from: Peek<'mem, 'facet>,
137✔
309
    to: Peek<'mem, 'facet>,
137✔
310
    options: &DiffOptions,
137✔
311
) -> Diff<'mem, 'facet> {
137✔
312
    let from_dyn = from.into_dynamic_value().unwrap();
137✔
313
    let to_dyn = to.into_dynamic_value().unwrap();
137✔
314

315
    let from_kind = from_dyn.kind();
137✔
316
    let to_kind = to_dyn.kind();
137✔
317

318
    // If kinds differ, just return Replace
319
    if from_kind != to_kind {
137✔
320
        return Diff::Replace { from, to };
3✔
321
    }
134✔
322

323
    match from_kind {
134✔
324
        DynValueKind::Null => Diff::Equal { value: Some(from) },
×
325
        DynValueKind::Bool => {
326
            if from_dyn.as_bool() == to_dyn.as_bool() {
6✔
327
                Diff::Equal { value: Some(from) }
×
328
            } else {
329
                Diff::Replace { from, to }
6✔
330
            }
331
        }
332
        DynValueKind::Number => {
333
            // Compare numbers - try exact integer comparison first, then float
334
            let same = match (from_dyn.as_i64(), to_dyn.as_i64()) {
66✔
335
                (Some(l), Some(r)) => l == r,
66✔
336
                _ => match (from_dyn.as_u64(), to_dyn.as_u64()) {
×
337
                    (Some(l), Some(r)) => l == r,
×
338
                    _ => match (from_dyn.as_f64(), to_dyn.as_f64()) {
×
339
                        (Some(l), Some(r)) => l == r,
×
340
                        _ => false,
×
341
                    },
342
                },
343
            };
344
            if same {
66✔
345
                Diff::Equal { value: Some(from) }
×
346
            } else {
347
                Diff::Replace { from, to }
66✔
348
            }
349
        }
350
        DynValueKind::String => {
351
            if from_dyn.as_str() == to_dyn.as_str() {
21✔
352
                Diff::Equal { value: Some(from) }
×
353
            } else {
354
                Diff::Replace { from, to }
21✔
355
            }
356
        }
357
        DynValueKind::Bytes => {
358
            if from_dyn.as_bytes() == to_dyn.as_bytes() {
×
359
                Diff::Equal { value: Some(from) }
×
360
            } else {
361
                Diff::Replace { from, to }
×
362
            }
363
        }
364
        DynValueKind::Array => {
365
            // Use the sequence diff algorithm for arrays
366
            let from_iter = from_dyn.array_iter();
6✔
367
            let to_iter = to_dyn.array_iter();
6✔
368

369
            let from_elems: Vec<_> = from_iter.map(|i| i.collect()).unwrap_or_default();
6✔
370
            let to_elems: Vec<_> = to_iter.map(|i| i.collect()).unwrap_or_default();
6✔
371

372
            let updates = sequences::diff_with_options(from_elems, to_elems, options);
6✔
373

374
            if updates.is_empty() {
6✔
375
                return Diff::Equal { value: Some(from) };
×
376
            }
6✔
377

378
            Diff::Sequence {
6✔
379
                from: from.shape(),
6✔
380
                to: to.shape(),
6✔
381
                updates,
6✔
382
            }
6✔
383
        }
384
        DynValueKind::Object => {
385
            // Treat objects like struct diffs
386
            let from_len = from_dyn.object_len().unwrap_or(0);
35✔
387
            let to_len = to_dyn.object_len().unwrap_or(0);
35✔
388

389
            let mut updates = HashMap::new();
35✔
390
            let mut deletions = HashMap::new();
35✔
391
            let mut insertions = HashMap::new();
35✔
392
            let mut unchanged = HashSet::new();
35✔
393

394
            // Collect keys from `from`
395
            let mut from_keys: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
35✔
396
            for i in 0..from_len {
74✔
397
                if let Some((key, value)) = from_dyn.object_get_entry(i) {
74✔
398
                    from_keys.insert(key.to_owned(), value);
74✔
399
                }
74✔
400
            }
401

402
            // Collect keys from `to`
403
            let mut to_keys: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
35✔
404
            for i in 0..to_len {
74✔
405
                if let Some((key, value)) = to_dyn.object_get_entry(i) {
74✔
406
                    to_keys.insert(key.to_owned(), value);
74✔
407
                }
74✔
408
            }
409

410
            // Compare entries
411
            for (key, from_value) in &from_keys {
74✔
412
                if let Some(to_value) = to_keys.get(key) {
74✔
413
                    let diff = diff_new_peek_with_options(*from_value, *to_value, options);
72✔
414
                    if diff.is_equal() {
72✔
415
                        unchanged.insert(Cow::Owned(key.clone()));
24✔
416
                    } else {
48✔
417
                        updates.insert(Cow::Owned(key.clone()), diff);
48✔
418
                    }
48✔
419
                } else {
2✔
420
                    deletions.insert(Cow::Owned(key.clone()), *from_value);
2✔
421
                }
2✔
422
            }
423

424
            for (key, to_value) in &to_keys {
74✔
425
                if !from_keys.contains_key(key) {
74✔
426
                    insertions.insert(Cow::Owned(key.clone()), *to_value);
2✔
427
                }
72✔
428
            }
429

430
            let is_empty = updates.is_empty() && deletions.is_empty() && insertions.is_empty();
35✔
431
            if is_empty {
35✔
432
                return Diff::Equal { value: Some(from) };
×
433
            }
35✔
434

435
            Diff::User {
35✔
436
                from: from.shape(),
35✔
437
                to: to.shape(),
35✔
438
                variant: None,
35✔
439
                value: Value::Struct {
35✔
440
                    updates,
35✔
441
                    deletions,
35✔
442
                    insertions,
35✔
443
                    unchanged,
35✔
444
                },
35✔
445
            }
35✔
446
        }
447
        DynValueKind::DateTime => {
448
            // Compare datetime by their components
449
            if from_dyn.as_datetime() == to_dyn.as_datetime() {
×
450
                Diff::Equal { value: Some(from) }
×
451
            } else {
452
                Diff::Replace { from, to }
×
453
            }
454
        }
455
        DynValueKind::QName | DynValueKind::Uuid => {
456
            // For QName and Uuid, compare by their raw representation
457
            // Since they have the same kind, we can only compare by Replace semantics
458
            Diff::Replace { from, to }
×
459
        }
460
    }
461
}
137✔
462

463
/// Diff a DynamicValue against a concrete type
464
/// `dyn_peek` is the DynamicValue, `concrete_peek` is the concrete type
465
/// `swapped` indicates if the original from/to were swapped (true means dyn_peek is actually "to")
466
fn diff_dynamic_vs_concrete<'mem, 'facet>(
273✔
467
    dyn_peek: Peek<'mem, 'facet>,
273✔
468
    concrete_peek: Peek<'mem, 'facet>,
273✔
469
    swapped: bool,
273✔
470
    options: &DiffOptions,
273✔
471
) -> Diff<'mem, 'facet> {
273✔
472
    // Determine actual from/to based on swapped flag
473
    let (from_peek, to_peek) = if swapped {
273✔
474
        (concrete_peek, dyn_peek)
63✔
475
    } else {
476
        (dyn_peek, concrete_peek)
210✔
477
    };
478
    let dyn_val = dyn_peek.into_dynamic_value().unwrap();
273✔
479
    let dyn_kind = dyn_val.kind();
273✔
480

481
    // Try to match based on the DynamicValue's kind
482
    match dyn_kind {
273✔
483
        DynValueKind::Bool => {
484
            if concrete_peek
5✔
485
                .get::<bool>()
5✔
486
                .ok()
5✔
487
                .is_some_and(|&v| dyn_val.as_bool() == Some(v))
5✔
488
            {
489
                return Diff::Equal {
3✔
490
                    value: Some(from_peek),
3✔
491
                };
3✔
492
            }
2✔
493
        }
494
        DynValueKind::Number => {
495
            let is_equal =
206✔
496
                // Try signed integers
497
                concrete_peek.get::<i8>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
206✔
498
                || concrete_peek.get::<i16>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
206✔
499
                || concrete_peek.get::<i32>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
206✔
500
                || concrete_peek.get::<i64>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v))
204✔
501
                || concrete_peek.get::<isize>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
129✔
502
                // Try unsigned integers
503
                || concrete_peek.get::<u8>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
129✔
504
                || concrete_peek.get::<u16>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
124✔
505
                || concrete_peek.get::<u32>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
124✔
506
                || concrete_peek.get::<u64>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v))
124✔
507
                || concrete_peek.get::<usize>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
124✔
508
                // Try floats
509
                || concrete_peek.get::<f32>().ok().is_some_and(|&v| dyn_val.as_f64() == Some(v as f64))
124✔
510
                || concrete_peek.get::<f64>().ok().is_some_and(|&v| dyn_val.as_f64() == Some(v));
124✔
511
            if is_equal {
206✔
512
                return Diff::Equal {
83✔
513
                    value: Some(from_peek),
83✔
514
                };
83✔
515
            }
123✔
516
        }
517
        DynValueKind::String => {
518
            if concrete_peek
24✔
519
                .as_str()
24✔
520
                .is_some_and(|s| dyn_val.as_str() == Some(s))
24✔
521
            {
522
                return Diff::Equal {
14✔
523
                    value: Some(from_peek),
14✔
524
                };
14✔
525
            }
10✔
526
        }
527
        DynValueKind::Array => {
528
            // Try to diff as sequences if the concrete type is list-like
529
            if let Ok(concrete_list) = concrete_peek.into_list_like() {
21✔
530
                let dyn_elems: Vec<_> = dyn_val
19✔
531
                    .array_iter()
19✔
532
                    .map(|i| i.collect())
19✔
533
                    .unwrap_or_default();
19✔
534
                let concrete_elems: Vec<_> = concrete_list.iter().collect();
19✔
535

536
                // Use correct order based on swapped flag
537
                let (from_elems, to_elems) = if swapped {
19✔
538
                    (concrete_elems, dyn_elems)
5✔
539
                } else {
540
                    (dyn_elems, concrete_elems)
14✔
541
                };
542
                let updates = sequences::diff_with_options(from_elems, to_elems, options);
19✔
543

544
                if updates.is_empty() {
19✔
545
                    return Diff::Equal {
10✔
546
                        value: Some(from_peek),
10✔
547
                    };
10✔
548
                }
9✔
549

550
                return Diff::Sequence {
9✔
551
                    from: from_peek.shape(),
9✔
552
                    to: to_peek.shape(),
9✔
553
                    updates,
9✔
554
                };
9✔
555
            }
2✔
556
        }
557
        DynValueKind::Object => {
558
            // Try to diff as struct if the concrete type is a struct
559
            if let Ok(concrete_struct) = concrete_peek.into_struct() {
16✔
560
                let dyn_len = dyn_val.object_len().unwrap_or(0);
16✔
561

562
                let mut updates = HashMap::new();
16✔
563
                let mut deletions = HashMap::new();
16✔
564
                let mut insertions = HashMap::new();
16✔
565
                let mut unchanged = HashSet::new();
16✔
566

567
                // Collect keys from dynamic object
568
                let mut dyn_keys: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
16✔
569
                for i in 0..dyn_len {
34✔
570
                    if let Some((key, value)) = dyn_val.object_get_entry(i) {
34✔
571
                        dyn_keys.insert(key.to_owned(), value);
34✔
572
                    }
34✔
573
                }
574

575
                // Compare with concrete struct fields
576
                // When swapped, dyn is "to" and concrete is "from", so we need to swap the diff direction
577
                for (key, dyn_value) in &dyn_keys {
34✔
578
                    if let Ok(concrete_value) = concrete_struct.field_by_name(key) {
34✔
579
                        let diff = if swapped {
34✔
580
                            diff_new_peek_with_options(concrete_value, *dyn_value, options)
2✔
581
                        } else {
582
                            diff_new_peek_with_options(*dyn_value, concrete_value, options)
32✔
583
                        };
584
                        if diff.is_equal() {
34✔
585
                            unchanged.insert(Cow::Owned(key.clone()));
17✔
586
                        } else {
17✔
587
                            updates.insert(Cow::Owned(key.clone()), diff);
17✔
588
                        }
17✔
589
                    } else {
590
                        // Field in dyn but not in concrete
591
                        // If swapped: dyn is "to", so this is an insertion
592
                        // If not swapped: dyn is "from", so this is a deletion
593
                        if swapped {
×
594
                            insertions.insert(Cow::Owned(key.clone()), *dyn_value);
×
595
                        } else {
×
596
                            deletions.insert(Cow::Owned(key.clone()), *dyn_value);
×
597
                        }
×
598
                    }
599
                }
600

601
                for (field, concrete_value) in concrete_struct.fields() {
34✔
602
                    if !dyn_keys.contains_key(field.name) {
34✔
603
                        // Field in concrete but not in dyn
604
                        // If swapped: concrete is "from", so this is a deletion
605
                        // If not swapped: concrete is "to", so this is an insertion
606
                        if swapped {
×
607
                            deletions.insert(Cow::Borrowed(field.name), concrete_value);
×
608
                        } else {
×
609
                            insertions.insert(Cow::Borrowed(field.name), concrete_value);
×
610
                        }
×
611
                    }
34✔
612
                }
613

614
                let is_empty = updates.is_empty() && deletions.is_empty() && insertions.is_empty();
16✔
615
                if is_empty {
16✔
616
                    return Diff::Equal {
6✔
617
                        value: Some(from_peek),
6✔
618
                    };
6✔
619
                }
10✔
620

621
                return Diff::User {
10✔
622
                    from: from_peek.shape(),
10✔
623
                    to: to_peek.shape(),
10✔
624
                    variant: None,
10✔
625
                    value: Value::Struct {
10✔
626
                        updates,
10✔
627
                        deletions,
10✔
628
                        insertions,
10✔
629
                        unchanged,
10✔
630
                    },
10✔
631
                };
10✔
632
            }
×
633
        }
634
        // For other kinds (Null, Bytes, DateTime), fall through to Replace
635
        _ => {}
1✔
636
    }
637

638
    Diff::Replace {
138✔
639
        from: from_peek,
138✔
640
        to: to_peek,
138✔
641
    }
138✔
642
}
273✔
643

644
/// Extract a float value from a Peek, handling both f32 and f64
645
fn try_extract_float(peek: Peek) -> Option<f64> {
40✔
646
    match peek.scalar_type()? {
40✔
647
        ScalarType::F64 => Some(*peek.get::<f64>().ok()?),
26✔
648
        ScalarType::F32 => Some(*peek.get::<f32>().ok()? as f64),
2✔
649
        _ => None,
2✔
650
    }
651
}
40✔
652

653
/// Check if two Peek values are equal within the specified float tolerance
654
fn check_float_tolerance(from: Peek, to: Peek, tolerance: f64) -> bool {
20✔
655
    match (try_extract_float(from), try_extract_float(to)) {
20✔
656
        (Some(f1), Some(f2)) => (f1 - f2).abs() <= tolerance,
14✔
657
        _ => false,
6✔
658
    }
659
}
20✔
660

661
/// Dereference a pointer/reference to get the underlying value
662
fn deref_if_pointer<'mem, 'facet>(peek: Peek<'mem, 'facet>) -> Peek<'mem, 'facet> {
1,148,366✔
663
    if let Ok(ptr) = peek.into_pointer()
1,148,366✔
664
        && let Some(target) = ptr.borrow_inner()
282✔
665
    {
666
        return deref_if_pointer(target);
282✔
667
    }
1,148,084✔
668
    peek
1,148,084✔
669
}
1,148,366✔
670

671
/// Collect all leaf-level changes with their paths.
672
///
673
/// This walks the diff tree recursively and collects every terminal change
674
/// (scalar replacements) along with the path to reach them. This is useful
675
/// for compact display: if there's only one leaf change deep in a tree,
676
/// you can show `path.to.field: old → new` instead of nested structure.
677
pub fn collect_leaf_changes<'mem, 'facet>(
×
678
    diff: &Diff<'mem, 'facet>,
×
679
) -> Vec<LeafChange<'mem, 'facet>> {
×
680
    let mut changes = Vec::new();
×
681
    collect_leaf_changes_inner(diff, Path::new(), &mut changes);
×
682
    changes
×
683
}
×
684

685
fn collect_leaf_changes_inner<'mem, 'facet>(
×
686
    diff: &Diff<'mem, 'facet>,
×
687
    path: Path,
×
688
    changes: &mut Vec<LeafChange<'mem, 'facet>>,
×
689
) {
×
690
    match diff {
×
691
        Diff::Equal { .. } => {
×
692
            // No change
×
693
        }
×
694
        Diff::Replace { from, to } => {
×
695
            // This is a leaf change
×
696
            changes.push(LeafChange {
×
697
                path,
×
698
                kind: LeafChangeKind::Replace {
×
699
                    from: *from,
×
700
                    to: *to,
×
701
                },
×
702
            });
×
703
        }
×
704
        Diff::User {
705
            value,
×
706
            variant,
×
707
            from,
×
708
            ..
709
        } => {
710
            // For Option::Some, skip the variant in the path since it's implied
711
            // (the value exists, so it's Some)
712
            let is_option = matches!(from.def, Def::Option(_));
×
713

714
            let base_path = if let Some(v) = variant {
×
715
                if is_option && *v == "Some" {
×
716
                    path // Skip "::Some" for options
×
717
                } else {
718
                    path.with(PathSegment::Variant(Cow::Borrowed(*v)))
×
719
                }
720
            } else {
721
                path
×
722
            };
723

724
            match value {
×
725
                Value::Struct {
726
                    updates,
×
727
                    deletions,
×
728
                    insertions,
×
729
                    ..
730
                } => {
731
                    // Recurse into field updates
732
                    for (field, diff) in updates {
×
733
                        let field_path = base_path.with(PathSegment::Field(field.clone()));
×
734
                        collect_leaf_changes_inner(diff, field_path, changes);
×
735
                    }
×
736
                    // Deletions are leaf changes
737
                    for (field, peek) in deletions {
×
738
                        let field_path = base_path.with(PathSegment::Field(field.clone()));
×
739
                        changes.push(LeafChange {
×
740
                            path: field_path,
×
741
                            kind: LeafChangeKind::Delete { value: *peek },
×
742
                        });
×
743
                    }
×
744
                    // Insertions are leaf changes
745
                    for (field, peek) in insertions {
×
746
                        let field_path = base_path.with(PathSegment::Field(field.clone()));
×
747
                        changes.push(LeafChange {
×
748
                            path: field_path,
×
749
                            kind: LeafChangeKind::Insert { value: *peek },
×
750
                        });
×
751
                    }
×
752
                }
753
                Value::Tuple { updates } => {
×
754
                    // For single-element tuples (like Option::Some), skip the index
755
                    if is_option {
×
756
                        // Recurse directly without adding [0]
×
757
                        collect_from_updates_for_single_elem(&base_path, updates, changes);
×
758
                    } else {
×
759
                        collect_from_updates(&base_path, updates, changes);
×
760
                    }
×
761
                }
762
            }
763
        }
764
        Diff::Sequence { updates, .. } => {
×
765
            collect_from_updates(&path, updates, changes);
×
766
        }
×
767
    }
768
}
×
769

770
/// Special handling for single-element tuples (like Option::Some)
771
/// where we want to skip the `[0]` index in the path.
772
fn collect_from_updates_for_single_elem<'mem, 'facet>(
×
773
    base_path: &Path,
×
774
    updates: &Updates<'mem, 'facet>,
×
775
    changes: &mut Vec<LeafChange<'mem, 'facet>>,
×
776
) {
×
777
    // For single-element tuples, we expect exactly one change
778
    // Just use base_path directly instead of adding [0]
779
    if let Some(update_group) = &updates.0.first {
×
780
        // Process the first replace group if present
781
        if let Some(replace) = &update_group.0.first
×
782
            && replace.removals.len() == 1
×
783
            && replace.additions.len() == 1
×
784
        {
785
            let from = replace.removals[0];
×
786
            let to = replace.additions[0];
×
787
            let nested = diff_new_peek(from, to);
×
788
            if matches!(nested, Diff::Replace { .. }) {
×
789
                changes.push(LeafChange {
×
790
                    path: base_path.clone(),
×
791
                    kind: LeafChangeKind::Replace { from, to },
×
792
                });
×
793
            } else {
×
794
                collect_leaf_changes_inner(&nested, base_path.clone(), changes);
×
795
            }
×
796
            return;
×
797
        }
×
798
        // Handle nested diffs
799
        if let Some(diffs) = &update_group.0.last {
×
800
            for diff in diffs {
×
801
                collect_leaf_changes_inner(diff, base_path.clone(), changes);
×
802
            }
×
803
            return;
×
804
        }
×
805
    }
×
806
    // Fallback: use regular handling
807
    collect_from_updates(base_path, updates, changes);
×
808
}
×
809

810
fn collect_from_updates<'mem, 'facet>(
×
811
    base_path: &Path,
×
812
    updates: &Updates<'mem, 'facet>,
×
813
    changes: &mut Vec<LeafChange<'mem, 'facet>>,
×
814
) {
×
815
    // Walk through the interspersed structure to collect changes with correct indices
816
    let mut index = 0;
×
817

818
    // Process first update group if present
819
    if let Some(update_group) = &updates.0.first {
×
820
        collect_from_update_group(base_path, update_group, &mut index, changes);
×
821
    }
×
822

823
    // Process interleaved (unchanged, update) pairs
824
    for (unchanged, update_group) in &updates.0.values {
×
825
        index += unchanged.len();
×
826
        collect_from_update_group(base_path, update_group, &mut index, changes);
×
827
    }
×
828

829
    // Trailing unchanged items don't add changes
830
}
×
831

832
fn collect_from_update_group<'mem, 'facet>(
×
833
    base_path: &Path,
×
834
    group: &crate::UpdatesGroup<'mem, 'facet>,
×
835
    index: &mut usize,
×
836
    changes: &mut Vec<LeafChange<'mem, 'facet>>,
×
837
) {
×
838
    // Process first replace group if present
839
    if let Some(replace) = &group.0.first {
×
840
        collect_from_replace_group(base_path, replace, index, changes);
×
841
    }
×
842

843
    // Process interleaved (diffs, replace) pairs
844
    for (diffs, replace) in &group.0.values {
×
845
        for diff in diffs {
×
846
            let elem_path = base_path.with(PathSegment::Index(*index));
×
847
            collect_leaf_changes_inner(diff, elem_path, changes);
×
848
            *index += 1;
×
849
        }
×
850
        collect_from_replace_group(base_path, replace, index, changes);
×
851
    }
852

853
    // Process trailing diffs
854
    if let Some(diffs) = &group.0.last {
×
855
        for diff in diffs {
×
856
            let elem_path = base_path.with(PathSegment::Index(*index));
×
857
            collect_leaf_changes_inner(diff, elem_path, changes);
×
858
            *index += 1;
×
859
        }
×
860
    }
×
861
}
×
862

863
fn collect_from_replace_group<'mem, 'facet>(
×
864
    base_path: &Path,
×
865
    group: &crate::ReplaceGroup<'mem, 'facet>,
×
866
    index: &mut usize,
×
867
    changes: &mut Vec<LeafChange<'mem, 'facet>>,
×
868
) {
×
869
    // For replace groups, we have removals and additions
870
    // If counts match, treat as 1:1 replacements at the same index
871
    // Otherwise, show as deletions followed by insertions
872

873
    if group.removals.len() == group.additions.len() {
×
874
        // 1:1 replacements
875
        for (from, to) in group.removals.iter().zip(group.additions.iter()) {
×
876
            let elem_path = base_path.with(PathSegment::Index(*index));
×
877
            // Check if this is actually a nested diff
878
            let nested = diff_new_peek(*from, *to);
×
879
            if matches!(nested, Diff::Replace { .. }) {
×
880
                changes.push(LeafChange {
×
881
                    path: elem_path,
×
882
                    kind: LeafChangeKind::Replace {
×
883
                        from: *from,
×
884
                        to: *to,
×
885
                    },
×
886
                });
×
887
            } else {
×
888
                collect_leaf_changes_inner(&nested, elem_path, changes);
×
889
            }
×
890
            *index += 1;
×
891
        }
892
    } else {
893
        // Mixed deletions and insertions
894
        for from in &group.removals {
×
895
            let elem_path = base_path.with(PathSegment::Index(*index));
×
896
            changes.push(LeafChange {
×
897
                path: elem_path.clone(),
×
898
                kind: LeafChangeKind::Delete { value: *from },
×
899
            });
×
900
            *index += 1;
×
901
        }
×
902
        // Insertions happen at current index
903
        for to in &group.additions {
×
904
            let elem_path = base_path.with(PathSegment::Index(*index));
×
905
            changes.push(LeafChange {
×
906
                path: elem_path,
×
907
                kind: LeafChangeKind::Insert { value: *to },
×
908
            });
×
909
            *index += 1;
×
910
        }
×
911
    }
912
}
×
913

914
/// A single leaf-level change in a diff, with path information.
915
#[derive(Debug, Clone)]
916
pub struct LeafChange<'mem, 'facet> {
917
    /// The path from root to this change
918
    pub path: Path,
919
    /// The kind of change
920
    pub kind: LeafChangeKind<'mem, 'facet>,
921
}
922

923
/// The kind of leaf change.
924
#[derive(Debug, Clone)]
925
pub enum LeafChangeKind<'mem, 'facet> {
926
    /// A value was replaced
927
    Replace {
928
        /// The old value
929
        from: Peek<'mem, 'facet>,
930
        /// The new value
931
        to: Peek<'mem, 'facet>,
932
    },
933
    /// A value was deleted
934
    Delete {
935
        /// The deleted value
936
        value: Peek<'mem, 'facet>,
937
    },
938
    /// A value was inserted
939
    Insert {
940
        /// The inserted value
941
        value: Peek<'mem, 'facet>,
942
    },
943
}
944

945
impl<'mem, 'facet> LeafChange<'mem, 'facet> {
946
    /// Format this change without colors.
947
    pub fn format_plain(&self) -> String {
×
948
        use facet_pretty::PrettyPrinter;
949

950
        let printer = PrettyPrinter::default()
×
951
            .with_colors(false)
×
952
            .with_minimal_option_names(true);
×
953

954
        let mut out = String::new();
×
955

956
        // Show path if non-empty
957
        if !self.path.0.is_empty() {
×
958
            out.push_str(&format!("{}: ", self.path));
×
959
        }
×
960

961
        match &self.kind {
×
962
            LeafChangeKind::Replace { from, to } => {
×
963
                out.push_str(&format!(
×
964
                    "{} → {}",
×
965
                    printer.format_peek(*from),
×
966
                    printer.format_peek(*to)
×
967
                ));
×
968
            }
×
969
            LeafChangeKind::Delete { value } => {
×
970
                out.push_str(&format!("- {}", printer.format_peek(*value)));
×
971
            }
×
972
            LeafChangeKind::Insert { value } => {
×
973
                out.push_str(&format!("+ {}", printer.format_peek(*value)));
×
974
            }
×
975
        }
976

977
        out
×
978
    }
×
979

980
    /// Format this change with colors.
981
    pub fn format_colored(&self) -> String {
×
982
        use facet_pretty::{PrettyPrinter, tokyo_night};
983
        use owo_colors::OwoColorize;
984

985
        let printer = PrettyPrinter::default()
×
986
            .with_colors(false)
×
987
            .with_minimal_option_names(true);
×
988

989
        let mut out = String::new();
×
990

991
        // Show path if non-empty (in field name color)
992
        if !self.path.0.is_empty() {
×
993
            out.push_str(&format!(
×
994
                "{}: ",
×
995
                format!("{}", self.path).color(tokyo_night::FIELD_NAME)
×
996
            ));
×
997
        }
×
998

999
        match &self.kind {
×
1000
            LeafChangeKind::Replace { from, to } => {
×
1001
                out.push_str(&format!(
×
1002
                    "{} {} {}",
×
1003
                    printer.format_peek(*from).color(tokyo_night::DELETION),
×
1004
                    "→".color(tokyo_night::COMMENT),
×
1005
                    printer.format_peek(*to).color(tokyo_night::INSERTION)
×
1006
                ));
×
1007
            }
×
1008
            LeafChangeKind::Delete { value } => {
×
1009
                out.push_str(&format!(
×
1010
                    "{} {}",
×
1011
                    "-".color(tokyo_night::DELETION),
×
1012
                    printer.format_peek(*value).color(tokyo_night::DELETION)
×
1013
                ));
×
1014
            }
×
1015
            LeafChangeKind::Insert { value } => {
×
1016
                out.push_str(&format!(
×
1017
                    "{} {}",
×
1018
                    "+".color(tokyo_night::INSERTION),
×
1019
                    printer.format_peek(*value).color(tokyo_night::INSERTION)
×
1020
                ));
×
1021
            }
×
1022
        }
1023

1024
        out
×
1025
    }
×
1026
}
1027

1028
impl<'mem, 'facet> std::fmt::Display for LeafChange<'mem, 'facet> {
1029
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
1030
        write!(f, "{}", self.format_plain())
×
1031
    }
×
1032
}
1033

1034
/// Configuration for diff formatting.
1035
#[derive(Debug, Clone)]
1036
pub struct DiffFormat {
1037
    /// Use colors in output
1038
    pub colors: bool,
1039
    /// Maximum number of changes before switching to summary mode
1040
    pub max_inline_changes: usize,
1041
    /// Whether to use compact (path-based) format for few changes
1042
    pub prefer_compact: bool,
1043
}
1044

1045
impl Default for DiffFormat {
1046
    fn default() -> Self {
×
1047
        Self {
×
1048
            colors: true,
×
1049
            max_inline_changes: 10,
×
1050
            prefer_compact: true,
×
1051
        }
×
1052
    }
×
1053
}
1054

1055
/// Format the diff with the given configuration.
1056
///
1057
/// This chooses between compact (path-based) and tree (nested) format
1058
/// based on the number of changes and the configuration.
1059
pub fn format_diff(diff: &Diff<'_, '_>, config: &DiffFormat) -> String {
×
1060
    if matches!(diff, Diff::Equal { .. }) {
×
1061
        return if config.colors {
×
1062
            use facet_pretty::tokyo_night;
1063
            use owo_colors::OwoColorize;
1064
            "(no changes)".color(tokyo_night::MUTED).to_string()
×
1065
        } else {
1066
            "(no changes)".to_string()
×
1067
        };
1068
    }
×
1069

1070
    let changes = collect_leaf_changes(diff);
×
1071

1072
    if changes.is_empty() {
×
1073
        return if config.colors {
×
1074
            use facet_pretty::tokyo_night;
1075
            use owo_colors::OwoColorize;
1076
            "(no changes)".color(tokyo_night::MUTED).to_string()
×
1077
        } else {
1078
            "(no changes)".to_string()
×
1079
        };
1080
    }
×
1081

1082
    // Use compact format if preferred and we have few changes
1083
    if config.prefer_compact && changes.len() <= config.max_inline_changes {
×
1084
        let mut out = String::new();
×
1085
        for (i, change) in changes.iter().enumerate() {
×
1086
            if i > 0 {
×
1087
                out.push('\n');
×
1088
            }
×
1089
            if config.colors {
×
1090
                out.push_str(&change.format_colored());
×
1091
            } else {
×
1092
                out.push_str(&change.format_plain());
×
1093
            }
×
1094
        }
1095
        return out;
×
1096
    }
×
1097

1098
    // Fall back to tree format for many changes
1099
    if changes.len() > config.max_inline_changes {
×
1100
        let mut out = String::new();
×
1101

1102
        // Show first few changes
1103
        for (i, change) in changes.iter().take(config.max_inline_changes).enumerate() {
×
1104
            if i > 0 {
×
1105
                out.push('\n');
×
1106
            }
×
1107
            if config.colors {
×
1108
                out.push_str(&change.format_colored());
×
1109
            } else {
×
1110
                out.push_str(&change.format_plain());
×
1111
            }
×
1112
        }
1113

1114
        // Show summary of remaining
1115
        let remaining = changes.len() - config.max_inline_changes;
×
1116
        if remaining > 0 {
×
1117
            out.push('\n');
×
1118
            let summary = format!(
×
1119
                "... and {} more change{}",
1120
                remaining,
1121
                if remaining == 1 { "" } else { "s" }
×
1122
            );
1123
            if config.colors {
×
1124
                use facet_pretty::tokyo_night;
1125
                use owo_colors::OwoColorize;
1126
                out.push_str(&summary.color(tokyo_night::MUTED).to_string());
×
1127
            } else {
×
1128
                out.push_str(&summary);
×
1129
            }
×
1130
        }
×
1131
        return out;
×
1132
    }
×
1133

1134
    // Default: use Display impl (tree format)
1135
    format!("{diff}")
×
1136
}
×
1137

1138
/// Format the diff with default configuration.
1139
pub fn format_diff_default(diff: &Diff<'_, '_>) -> String {
×
1140
    format_diff(diff, &DiffFormat::default())
×
1141
}
×
1142

1143
/// Format the diff in compact mode (path-based, no tree structure).
1144
pub fn format_diff_compact(diff: &Diff<'_, '_>) -> String {
×
1145
    format_diff(
×
1146
        diff,
×
1147
        &DiffFormat {
×
1148
            prefer_compact: true,
×
1149
            max_inline_changes: usize::MAX,
×
1150
            ..Default::default()
×
1151
        },
×
1152
    )
1153
}
×
1154

1155
/// Format the diff in compact mode without colors.
1156
pub fn format_diff_compact_plain(diff: &Diff<'_, '_>) -> String {
×
1157
    format_diff(
×
1158
        diff,
×
1159
        &DiffFormat {
×
1160
            colors: false,
×
1161
            prefer_compact: true,
×
1162
            max_inline_changes: usize::MAX,
×
1163
        },
×
1164
    )
1165
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc