• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

facet-rs / facet / 20188025685

13 Dec 2025 06:15AM UTC coverage: 58.211% (+0.2%) from 57.993%
20188025685

push

github

fasterthanlime
Expand format suite with serialization fixes and new test cases

- Add Set serialization support to facet-format serializer
- Fix isize/usize deserialization by checking type_identifier
- Add Cow<str> deserialization support in facet-value
- Add try_borrow_inner for NonZero types to enable serialization
- Add Set comparison support to facet-diff

New test cases (34 total):
- Arc/Rc smart pointer wrappers
- BTreeSet collection
- Extended numeric types (i16, u16, i128, u128, isize, usize)
- NonZero integer types
- Cow<'static, str> string fields

Skip XML i128 test (VNumber can't hold values outside i64/u64 range)

200 of 280 new or added lines in 5 files covered. (71.43%)

6 existing lines in 2 files now uncovered.

31918 of 54832 relevant lines covered (58.21%)

5787.9 hits per line

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

53.26
/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 {
171✔
26
        Self::default()
171✔
27
    }
171✔
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,442✔
58
    from: Peek<'mem, 'facet>,
574,442✔
59
    to: Peek<'mem, 'facet>,
574,442✔
60
    options: &DiffOptions,
574,442✔
61
) -> Diff<'mem, 'facet> {
574,442✔
62
    // Dereference pointers/references to compare the underlying values
63
    let from = deref_if_pointer(from);
574,442✔
64
    let to = deref_if_pointer(to);
574,442✔
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,442✔
70
    let from_has_partialeq = from.shape().is_partial_eq();
574,442✔
71
    let to_has_partialeq = to.shape().is_partial_eq();
574,442✔
72
    let values_equal = from == to;
574,442✔
73

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

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

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

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

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

117
                for (field, from) in from_ty.fields() {
382,599✔
118
                    if let Ok(to) = to_ty.field_by_name(field.name) {
382,599✔
119
                        let diff = diff_new_peek_with_options(from, to, options);
382,598✔
120
                        if diff.is_equal() {
382,598✔
121
                            unchanged.insert(Cow::Borrowed(field.name));
381,531✔
122
                        } else {
381,531✔
123
                            updates.insert(Cow::Borrowed(field.name), diff);
1,067✔
124
                        }
1,067✔
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,599✔
131
                    if from_ty.field_by_name(field.name).is_err() {
382,599✔
132
                        insertions.insert(Cow::Borrowed(field.name), to);
1✔
133
                    }
382,598✔
134
                }
135
                Value::Struct {
189,701✔
136
                    updates,
189,701✔
137
                    deletions,
189,701✔
138
                    insertions,
189,701✔
139
                    unchanged,
189,701✔
140
                }
189,701✔
141
            };
142

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

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

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

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

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

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

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

191
                    for (field, from) in from_enum.fields() {
314✔
192
                        if let Ok(Some(to)) = to_enum.field_by_name(field.name) {
27✔
193
                            let diff = diff_new_peek_with_options(from, to, options);
27✔
194
                            if diff.is_equal() {
27✔
195
                                unchanged.insert(Cow::Borrowed(field.name));
26✔
196
                            } else {
26✔
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() {
314✔
205
                        if !from_enum
27✔
206
                            .field_by_name(field.name)
27✔
207
                            .is_ok_and(|x| x.is_some())
27✔
208
                        {
×
209
                            insertions.insert(Cow::Borrowed(field.name), to);
×
210
                        }
27✔
211
                    }
212

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

221
            // If there are no changes, return Equal instead of User
222
            let is_empty = match &value {
928✔
223
                Value::Tuple { updates } => updates.is_empty(),
614✔
224
                Value::Struct {
225
                    updates,
314✔
226
                    deletions,
314✔
227
                    insertions,
314✔
228
                    ..
229
                } => updates.is_empty() && deletions.is_empty() && insertions.is_empty(),
314✔
230
            };
231
            if is_empty {
928✔
232
                return Diff::Equal { value: Some(from) };
647✔
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,762✔
270
            let to_list = to.into_list_like().unwrap();
188,762✔
271

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

278
            if updates.is_empty() {
188,762✔
279
                return Diff::Equal { value: Some(from) };
188,611✔
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::Map(_), _), (Def::Map(_), _)) => {
289
            let from_map = from.into_map().unwrap();
4✔
290
            let to_map = to.into_map().unwrap();
4✔
291

292
            let mut updates = HashMap::new();
4✔
293
            let mut deletions = HashMap::new();
4✔
294
            let mut insertions = HashMap::new();
4✔
295
            let mut unchanged = HashSet::new();
4✔
296

297
            // Collect entries from `from` map with string keys for comparison
298
            let mut from_entries: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
4✔
299
            for (key, value) in from_map.iter() {
8✔
300
                let key_str = format!("{:?}", key);
8✔
301
                from_entries.insert(key_str, value);
8✔
302
            }
8✔
303

304
            // Collect entries from `to` map
305
            let mut to_entries: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
4✔
306
            for (key, value) in to_map.iter() {
8✔
307
                let key_str = format!("{:?}", key);
8✔
308
                to_entries.insert(key_str, value);
8✔
309
            }
8✔
310

311
            // Compare entries
312
            for (key, from_value) in &from_entries {
8✔
313
                if let Some(to_value) = to_entries.get(key) {
8✔
314
                    let diff = diff_new_peek_with_options(*from_value, *to_value, options);
8✔
315
                    if diff.is_equal() {
8✔
316
                        unchanged.insert(Cow::Owned(key.clone()));
8✔
317
                    } else {
8✔
NEW
318
                        updates.insert(Cow::Owned(key.clone()), diff);
×
NEW
319
                    }
×
NEW
320
                } else {
×
NEW
321
                    deletions.insert(Cow::Owned(key.clone()), *from_value);
×
NEW
322
                }
×
323
            }
324

325
            for (key, to_value) in &to_entries {
8✔
326
                if !from_entries.contains_key(key) {
8✔
NEW
327
                    insertions.insert(Cow::Owned(key.clone()), *to_value);
×
328
                }
8✔
329
            }
330

331
            let is_empty = updates.is_empty() && deletions.is_empty() && insertions.is_empty();
4✔
332
            if is_empty {
4✔
333
                return Diff::Equal { value: Some(from) };
4✔
NEW
334
            }
×
335

NEW
336
            Diff::User {
×
NEW
337
                from: from.shape(),
×
NEW
338
                to: to.shape(),
×
NEW
339
                variant: None,
×
NEW
340
                value: Value::Struct {
×
NEW
341
                    updates,
×
NEW
342
                    deletions,
×
NEW
343
                    insertions,
×
NEW
344
                    unchanged,
×
NEW
345
                },
×
NEW
346
            }
×
347
        }
348
        ((Def::Set(_), _), (Def::Set(_), _)) => {
349
            let from_set = from.into_set().unwrap();
4✔
350
            let to_set = to.into_set().unwrap();
4✔
351

352
            // Collect items from both sets using debug format for comparison
353
            let mut from_items: HashSet<String> = HashSet::new();
4✔
354
            for item in from_set.iter() {
12✔
355
                from_items.insert(format!("{:?}", item));
12✔
356
            }
12✔
357

358
            let mut to_items: HashSet<String> = HashSet::new();
4✔
359
            for item in to_set.iter() {
12✔
360
                to_items.insert(format!("{:?}", item));
12✔
361
            }
12✔
362

363
            // Sets are equal if they have the same items
364
            if from_items == to_items {
4✔
365
                return Diff::Equal { value: Some(from) };
4✔
NEW
366
            }
×
367

NEW
368
            Diff::Replace { from, to }
×
369
        }
370
        ((Def::DynamicValue(_), _), (Def::DynamicValue(_), _)) => {
371
            diff_dynamic_values(from, to, options)
137✔
372
        }
373
        // DynamicValue vs concrete type
374
        ((Def::DynamicValue(_), _), _) => diff_dynamic_vs_concrete(from, to, false, options),
210✔
375
        (_, (Def::DynamicValue(_), _)) => diff_dynamic_vs_concrete(to, from, true, options),
63✔
376
        _ => Diff::Replace { from, to },
1,897✔
377
    }
378
}
574,442✔
379

380
/// Computes the difference between two `Peek` values (backward compatibility wrapper)
381
pub fn diff_new_peek<'mem, 'facet>(
221✔
382
    from: Peek<'mem, 'facet>,
221✔
383
    to: Peek<'mem, 'facet>,
221✔
384
) -> Diff<'mem, 'facet> {
221✔
385
    diff_new_peek_with_options(from, to, &DiffOptions::default())
221✔
386
}
221✔
387

388
/// Diff two dynamic values (like `facet_value::Value`)
389
fn diff_dynamic_values<'mem, 'facet>(
137✔
390
    from: Peek<'mem, 'facet>,
137✔
391
    to: Peek<'mem, 'facet>,
137✔
392
    options: &DiffOptions,
137✔
393
) -> Diff<'mem, 'facet> {
137✔
394
    let from_dyn = from.into_dynamic_value().unwrap();
137✔
395
    let to_dyn = to.into_dynamic_value().unwrap();
137✔
396

397
    let from_kind = from_dyn.kind();
137✔
398
    let to_kind = to_dyn.kind();
137✔
399

400
    // If kinds differ, just return Replace
401
    if from_kind != to_kind {
137✔
402
        return Diff::Replace { from, to };
3✔
403
    }
134✔
404

405
    match from_kind {
134✔
406
        DynValueKind::Null => Diff::Equal { value: Some(from) },
×
407
        DynValueKind::Bool => {
408
            if from_dyn.as_bool() == to_dyn.as_bool() {
6✔
409
                Diff::Equal { value: Some(from) }
×
410
            } else {
411
                Diff::Replace { from, to }
6✔
412
            }
413
        }
414
        DynValueKind::Number => {
415
            // Compare numbers - try exact integer comparison first, then float
416
            let same = match (from_dyn.as_i64(), to_dyn.as_i64()) {
66✔
417
                (Some(l), Some(r)) => l == r,
66✔
418
                _ => match (from_dyn.as_u64(), to_dyn.as_u64()) {
×
419
                    (Some(l), Some(r)) => l == r,
×
420
                    _ => match (from_dyn.as_f64(), to_dyn.as_f64()) {
×
421
                        (Some(l), Some(r)) => l == r,
×
422
                        _ => false,
×
423
                    },
424
                },
425
            };
426
            if same {
66✔
427
                Diff::Equal { value: Some(from) }
×
428
            } else {
429
                Diff::Replace { from, to }
66✔
430
            }
431
        }
432
        DynValueKind::String => {
433
            if from_dyn.as_str() == to_dyn.as_str() {
21✔
434
                Diff::Equal { value: Some(from) }
×
435
            } else {
436
                Diff::Replace { from, to }
21✔
437
            }
438
        }
439
        DynValueKind::Bytes => {
440
            if from_dyn.as_bytes() == to_dyn.as_bytes() {
×
441
                Diff::Equal { value: Some(from) }
×
442
            } else {
443
                Diff::Replace { from, to }
×
444
            }
445
        }
446
        DynValueKind::Array => {
447
            // Use the sequence diff algorithm for arrays
448
            let from_iter = from_dyn.array_iter();
6✔
449
            let to_iter = to_dyn.array_iter();
6✔
450

451
            let from_elems: Vec<_> = from_iter.map(|i| i.collect()).unwrap_or_default();
6✔
452
            let to_elems: Vec<_> = to_iter.map(|i| i.collect()).unwrap_or_default();
6✔
453

454
            let updates = sequences::diff_with_options(from_elems, to_elems, options);
6✔
455

456
            if updates.is_empty() {
6✔
457
                return Diff::Equal { value: Some(from) };
×
458
            }
6✔
459

460
            Diff::Sequence {
6✔
461
                from: from.shape(),
6✔
462
                to: to.shape(),
6✔
463
                updates,
6✔
464
            }
6✔
465
        }
466
        DynValueKind::Object => {
467
            // Treat objects like struct diffs
468
            let from_len = from_dyn.object_len().unwrap_or(0);
35✔
469
            let to_len = to_dyn.object_len().unwrap_or(0);
35✔
470

471
            let mut updates = HashMap::new();
35✔
472
            let mut deletions = HashMap::new();
35✔
473
            let mut insertions = HashMap::new();
35✔
474
            let mut unchanged = HashSet::new();
35✔
475

476
            // Collect keys from `from`
477
            let mut from_keys: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
35✔
478
            for i in 0..from_len {
74✔
479
                if let Some((key, value)) = from_dyn.object_get_entry(i) {
74✔
480
                    from_keys.insert(key.to_owned(), value);
74✔
481
                }
74✔
482
            }
483

484
            // Collect keys from `to`
485
            let mut to_keys: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
35✔
486
            for i in 0..to_len {
74✔
487
                if let Some((key, value)) = to_dyn.object_get_entry(i) {
74✔
488
                    to_keys.insert(key.to_owned(), value);
74✔
489
                }
74✔
490
            }
491

492
            // Compare entries
493
            for (key, from_value) in &from_keys {
74✔
494
                if let Some(to_value) = to_keys.get(key) {
74✔
495
                    let diff = diff_new_peek_with_options(*from_value, *to_value, options);
72✔
496
                    if diff.is_equal() {
72✔
497
                        unchanged.insert(Cow::Owned(key.clone()));
24✔
498
                    } else {
48✔
499
                        updates.insert(Cow::Owned(key.clone()), diff);
48✔
500
                    }
48✔
501
                } else {
2✔
502
                    deletions.insert(Cow::Owned(key.clone()), *from_value);
2✔
503
                }
2✔
504
            }
505

506
            for (key, to_value) in &to_keys {
74✔
507
                if !from_keys.contains_key(key) {
74✔
508
                    insertions.insert(Cow::Owned(key.clone()), *to_value);
2✔
509
                }
72✔
510
            }
511

512
            let is_empty = updates.is_empty() && deletions.is_empty() && insertions.is_empty();
35✔
513
            if is_empty {
35✔
514
                return Diff::Equal { value: Some(from) };
×
515
            }
35✔
516

517
            Diff::User {
35✔
518
                from: from.shape(),
35✔
519
                to: to.shape(),
35✔
520
                variant: None,
35✔
521
                value: Value::Struct {
35✔
522
                    updates,
35✔
523
                    deletions,
35✔
524
                    insertions,
35✔
525
                    unchanged,
35✔
526
                },
35✔
527
            }
35✔
528
        }
529
        DynValueKind::DateTime => {
530
            // Compare datetime by their components
531
            if from_dyn.as_datetime() == to_dyn.as_datetime() {
×
532
                Diff::Equal { value: Some(from) }
×
533
            } else {
534
                Diff::Replace { from, to }
×
535
            }
536
        }
537
        DynValueKind::QName | DynValueKind::Uuid => {
538
            // For QName and Uuid, compare by their raw representation
539
            // Since they have the same kind, we can only compare by Replace semantics
540
            Diff::Replace { from, to }
×
541
        }
542
    }
543
}
137✔
544

545
/// Diff a DynamicValue against a concrete type
546
/// `dyn_peek` is the DynamicValue, `concrete_peek` is the concrete type
547
/// `swapped` indicates if the original from/to were swapped (true means dyn_peek is actually "to")
548
fn diff_dynamic_vs_concrete<'mem, 'facet>(
273✔
549
    dyn_peek: Peek<'mem, 'facet>,
273✔
550
    concrete_peek: Peek<'mem, 'facet>,
273✔
551
    swapped: bool,
273✔
552
    options: &DiffOptions,
273✔
553
) -> Diff<'mem, 'facet> {
273✔
554
    // Determine actual from/to based on swapped flag
555
    let (from_peek, to_peek) = if swapped {
273✔
556
        (concrete_peek, dyn_peek)
63✔
557
    } else {
558
        (dyn_peek, concrete_peek)
210✔
559
    };
560
    let dyn_val = dyn_peek.into_dynamic_value().unwrap();
273✔
561
    let dyn_kind = dyn_val.kind();
273✔
562

563
    // Try to match based on the DynamicValue's kind
564
    match dyn_kind {
273✔
565
        DynValueKind::Bool => {
566
            if concrete_peek
5✔
567
                .get::<bool>()
5✔
568
                .ok()
5✔
569
                .is_some_and(|&v| dyn_val.as_bool() == Some(v))
5✔
570
            {
571
                return Diff::Equal {
3✔
572
                    value: Some(from_peek),
3✔
573
                };
3✔
574
            }
2✔
575
        }
576
        DynValueKind::Number => {
577
            let is_equal =
206✔
578
                // Try signed integers
579
                concrete_peek.get::<i8>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
206✔
580
                || concrete_peek.get::<i16>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
206✔
581
                || concrete_peek.get::<i32>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
206✔
582
                || concrete_peek.get::<i64>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v))
204✔
583
                || concrete_peek.get::<isize>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
129✔
584
                // Try unsigned integers
585
                || concrete_peek.get::<u8>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
129✔
586
                || concrete_peek.get::<u16>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
124✔
587
                || concrete_peek.get::<u32>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
124✔
588
                || concrete_peek.get::<u64>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v))
124✔
589
                || concrete_peek.get::<usize>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
124✔
590
                // Try floats
591
                || concrete_peek.get::<f32>().ok().is_some_and(|&v| dyn_val.as_f64() == Some(v as f64))
124✔
592
                || concrete_peek.get::<f64>().ok().is_some_and(|&v| dyn_val.as_f64() == Some(v));
124✔
593
            if is_equal {
206✔
594
                return Diff::Equal {
83✔
595
                    value: Some(from_peek),
83✔
596
                };
83✔
597
            }
123✔
598
        }
599
        DynValueKind::String => {
600
            if concrete_peek
24✔
601
                .as_str()
24✔
602
                .is_some_and(|s| dyn_val.as_str() == Some(s))
24✔
603
            {
604
                return Diff::Equal {
14✔
605
                    value: Some(from_peek),
14✔
606
                };
14✔
607
            }
10✔
608
        }
609
        DynValueKind::Array => {
610
            // Try to diff as sequences if the concrete type is list-like
611
            if let Ok(concrete_list) = concrete_peek.into_list_like() {
21✔
612
                let dyn_elems: Vec<_> = dyn_val
19✔
613
                    .array_iter()
19✔
614
                    .map(|i| i.collect())
19✔
615
                    .unwrap_or_default();
19✔
616
                let concrete_elems: Vec<_> = concrete_list.iter().collect();
19✔
617

618
                // Use correct order based on swapped flag
619
                let (from_elems, to_elems) = if swapped {
19✔
620
                    (concrete_elems, dyn_elems)
5✔
621
                } else {
622
                    (dyn_elems, concrete_elems)
14✔
623
                };
624
                let updates = sequences::diff_with_options(from_elems, to_elems, options);
19✔
625

626
                if updates.is_empty() {
19✔
627
                    return Diff::Equal {
10✔
628
                        value: Some(from_peek),
10✔
629
                    };
10✔
630
                }
9✔
631

632
                return Diff::Sequence {
9✔
633
                    from: from_peek.shape(),
9✔
634
                    to: to_peek.shape(),
9✔
635
                    updates,
9✔
636
                };
9✔
637
            }
2✔
638
        }
639
        DynValueKind::Object => {
640
            // Try to diff as struct if the concrete type is a struct
641
            if let Ok(concrete_struct) = concrete_peek.into_struct() {
16✔
642
                let dyn_len = dyn_val.object_len().unwrap_or(0);
16✔
643

644
                let mut updates = HashMap::new();
16✔
645
                let mut deletions = HashMap::new();
16✔
646
                let mut insertions = HashMap::new();
16✔
647
                let mut unchanged = HashSet::new();
16✔
648

649
                // Collect keys from dynamic object
650
                let mut dyn_keys: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
16✔
651
                for i in 0..dyn_len {
34✔
652
                    if let Some((key, value)) = dyn_val.object_get_entry(i) {
34✔
653
                        dyn_keys.insert(key.to_owned(), value);
34✔
654
                    }
34✔
655
                }
656

657
                // Compare with concrete struct fields
658
                // When swapped, dyn is "to" and concrete is "from", so we need to swap the diff direction
659
                for (key, dyn_value) in &dyn_keys {
34✔
660
                    if let Ok(concrete_value) = concrete_struct.field_by_name(key) {
34✔
661
                        let diff = if swapped {
34✔
662
                            diff_new_peek_with_options(concrete_value, *dyn_value, options)
2✔
663
                        } else {
664
                            diff_new_peek_with_options(*dyn_value, concrete_value, options)
32✔
665
                        };
666
                        if diff.is_equal() {
34✔
667
                            unchanged.insert(Cow::Owned(key.clone()));
17✔
668
                        } else {
17✔
669
                            updates.insert(Cow::Owned(key.clone()), diff);
17✔
670
                        }
17✔
671
                    } else {
672
                        // Field in dyn but not in concrete
673
                        // If swapped: dyn is "to", so this is an insertion
674
                        // If not swapped: dyn is "from", so this is a deletion
675
                        if swapped {
×
676
                            insertions.insert(Cow::Owned(key.clone()), *dyn_value);
×
677
                        } else {
×
678
                            deletions.insert(Cow::Owned(key.clone()), *dyn_value);
×
679
                        }
×
680
                    }
681
                }
682

683
                for (field, concrete_value) in concrete_struct.fields() {
34✔
684
                    if !dyn_keys.contains_key(field.name) {
34✔
685
                        // Field in concrete but not in dyn
686
                        // If swapped: concrete is "from", so this is a deletion
687
                        // If not swapped: concrete is "to", so this is an insertion
688
                        if swapped {
×
689
                            deletions.insert(Cow::Borrowed(field.name), concrete_value);
×
690
                        } else {
×
691
                            insertions.insert(Cow::Borrowed(field.name), concrete_value);
×
692
                        }
×
693
                    }
34✔
694
                }
695

696
                let is_empty = updates.is_empty() && deletions.is_empty() && insertions.is_empty();
16✔
697
                if is_empty {
16✔
698
                    return Diff::Equal {
6✔
699
                        value: Some(from_peek),
6✔
700
                    };
6✔
701
                }
10✔
702

703
                return Diff::User {
10✔
704
                    from: from_peek.shape(),
10✔
705
                    to: to_peek.shape(),
10✔
706
                    variant: None,
10✔
707
                    value: Value::Struct {
10✔
708
                        updates,
10✔
709
                        deletions,
10✔
710
                        insertions,
10✔
711
                        unchanged,
10✔
712
                    },
10✔
713
                };
10✔
714
            }
×
715
        }
716
        // For other kinds (Null, Bytes, DateTime), fall through to Replace
717
        _ => {}
1✔
718
    }
719

720
    Diff::Replace {
138✔
721
        from: from_peek,
138✔
722
        to: to_peek,
138✔
723
    }
138✔
724
}
273✔
725

726
/// Extract a float value from a Peek, handling both f32 and f64
727
fn try_extract_float(peek: Peek) -> Option<f64> {
40✔
728
    match peek.scalar_type()? {
40✔
729
        ScalarType::F64 => Some(*peek.get::<f64>().ok()?),
26✔
730
        ScalarType::F32 => Some(*peek.get::<f32>().ok()? as f64),
2✔
731
        _ => None,
2✔
732
    }
733
}
40✔
734

735
/// Check if two Peek values are equal within the specified float tolerance
736
fn check_float_tolerance(from: Peek, to: Peek, tolerance: f64) -> bool {
20✔
737
    match (try_extract_float(from), try_extract_float(to)) {
20✔
738
        (Some(f1), Some(f2)) => (f1 - f2).abs() <= tolerance,
14✔
739
        _ => false,
6✔
740
    }
741
}
20✔
742

743
/// Dereference a pointer/reference to get the underlying value
744
fn deref_if_pointer<'mem, 'facet>(peek: Peek<'mem, 'facet>) -> Peek<'mem, 'facet> {
1,149,206✔
745
    if let Ok(ptr) = peek.into_pointer()
1,149,206✔
746
        && let Some(target) = ptr.borrow_inner()
322✔
747
    {
748
        return deref_if_pointer(target);
322✔
749
    }
1,148,884✔
750
    peek
1,148,884✔
751
}
1,149,206✔
752

753
/// Collect all leaf-level changes with their paths.
754
///
755
/// This walks the diff tree recursively and collects every terminal change
756
/// (scalar replacements) along with the path to reach them. This is useful
757
/// for compact display: if there's only one leaf change deep in a tree,
758
/// you can show `path.to.field: old → new` instead of nested structure.
759
pub fn collect_leaf_changes<'mem, 'facet>(
×
760
    diff: &Diff<'mem, 'facet>,
×
761
) -> Vec<LeafChange<'mem, 'facet>> {
×
762
    let mut changes = Vec::new();
×
763
    collect_leaf_changes_inner(diff, Path::new(), &mut changes);
×
764
    changes
×
765
}
×
766

767
fn collect_leaf_changes_inner<'mem, 'facet>(
×
768
    diff: &Diff<'mem, 'facet>,
×
769
    path: Path,
×
770
    changes: &mut Vec<LeafChange<'mem, 'facet>>,
×
771
) {
×
772
    match diff {
×
773
        Diff::Equal { .. } => {
×
774
            // No change
×
775
        }
×
776
        Diff::Replace { from, to } => {
×
777
            // This is a leaf change
×
778
            changes.push(LeafChange {
×
779
                path,
×
780
                kind: LeafChangeKind::Replace {
×
781
                    from: *from,
×
782
                    to: *to,
×
783
                },
×
784
            });
×
785
        }
×
786
        Diff::User {
787
            value,
×
788
            variant,
×
789
            from,
×
790
            ..
791
        } => {
792
            // For Option::Some, skip the variant in the path since it's implied
793
            // (the value exists, so it's Some)
794
            let is_option = matches!(from.def, Def::Option(_));
×
795

796
            let base_path = if let Some(v) = variant {
×
797
                if is_option && *v == "Some" {
×
798
                    path // Skip "::Some" for options
×
799
                } else {
800
                    path.with(PathSegment::Variant(Cow::Borrowed(*v)))
×
801
                }
802
            } else {
803
                path
×
804
            };
805

806
            match value {
×
807
                Value::Struct {
808
                    updates,
×
809
                    deletions,
×
810
                    insertions,
×
811
                    ..
812
                } => {
813
                    // Recurse into field updates
814
                    for (field, diff) in updates {
×
815
                        let field_path = base_path.with(PathSegment::Field(field.clone()));
×
816
                        collect_leaf_changes_inner(diff, field_path, changes);
×
817
                    }
×
818
                    // Deletions are leaf changes
819
                    for (field, peek) in deletions {
×
820
                        let field_path = base_path.with(PathSegment::Field(field.clone()));
×
821
                        changes.push(LeafChange {
×
822
                            path: field_path,
×
823
                            kind: LeafChangeKind::Delete { value: *peek },
×
824
                        });
×
825
                    }
×
826
                    // Insertions are leaf changes
827
                    for (field, peek) in insertions {
×
828
                        let field_path = base_path.with(PathSegment::Field(field.clone()));
×
829
                        changes.push(LeafChange {
×
830
                            path: field_path,
×
831
                            kind: LeafChangeKind::Insert { value: *peek },
×
832
                        });
×
833
                    }
×
834
                }
835
                Value::Tuple { updates } => {
×
836
                    // For single-element tuples (like Option::Some), skip the index
837
                    if is_option {
×
838
                        // Recurse directly without adding [0]
×
839
                        collect_from_updates_for_single_elem(&base_path, updates, changes);
×
840
                    } else {
×
841
                        collect_from_updates(&base_path, updates, changes);
×
842
                    }
×
843
                }
844
            }
845
        }
846
        Diff::Sequence { updates, .. } => {
×
847
            collect_from_updates(&path, updates, changes);
×
848
        }
×
849
    }
850
}
×
851

852
/// Special handling for single-element tuples (like Option::Some)
853
/// where we want to skip the `[0]` index in the path.
854
fn collect_from_updates_for_single_elem<'mem, 'facet>(
×
855
    base_path: &Path,
×
856
    updates: &Updates<'mem, 'facet>,
×
857
    changes: &mut Vec<LeafChange<'mem, 'facet>>,
×
858
) {
×
859
    // For single-element tuples, we expect exactly one change
860
    // Just use base_path directly instead of adding [0]
861
    if let Some(update_group) = &updates.0.first {
×
862
        // Process the first replace group if present
863
        if let Some(replace) = &update_group.0.first
×
864
            && replace.removals.len() == 1
×
865
            && replace.additions.len() == 1
×
866
        {
867
            let from = replace.removals[0];
×
868
            let to = replace.additions[0];
×
869
            let nested = diff_new_peek(from, to);
×
870
            if matches!(nested, Diff::Replace { .. }) {
×
871
                changes.push(LeafChange {
×
872
                    path: base_path.clone(),
×
873
                    kind: LeafChangeKind::Replace { from, to },
×
874
                });
×
875
            } else {
×
876
                collect_leaf_changes_inner(&nested, base_path.clone(), changes);
×
877
            }
×
878
            return;
×
879
        }
×
880
        // Handle nested diffs
881
        if let Some(diffs) = &update_group.0.last {
×
882
            for diff in diffs {
×
883
                collect_leaf_changes_inner(diff, base_path.clone(), changes);
×
884
            }
×
885
            return;
×
886
        }
×
887
    }
×
888
    // Fallback: use regular handling
889
    collect_from_updates(base_path, updates, changes);
×
890
}
×
891

892
fn collect_from_updates<'mem, 'facet>(
×
893
    base_path: &Path,
×
894
    updates: &Updates<'mem, 'facet>,
×
895
    changes: &mut Vec<LeafChange<'mem, 'facet>>,
×
896
) {
×
897
    // Walk through the interspersed structure to collect changes with correct indices
898
    let mut index = 0;
×
899

900
    // Process first update group if present
901
    if let Some(update_group) = &updates.0.first {
×
902
        collect_from_update_group(base_path, update_group, &mut index, changes);
×
903
    }
×
904

905
    // Process interleaved (unchanged, update) pairs
906
    for (unchanged, update_group) in &updates.0.values {
×
907
        index += unchanged.len();
×
908
        collect_from_update_group(base_path, update_group, &mut index, changes);
×
909
    }
×
910

911
    // Trailing unchanged items don't add changes
912
}
×
913

914
fn collect_from_update_group<'mem, 'facet>(
×
915
    base_path: &Path,
×
916
    group: &crate::UpdatesGroup<'mem, 'facet>,
×
917
    index: &mut usize,
×
918
    changes: &mut Vec<LeafChange<'mem, 'facet>>,
×
919
) {
×
920
    // Process first replace group if present
921
    if let Some(replace) = &group.0.first {
×
922
        collect_from_replace_group(base_path, replace, index, changes);
×
923
    }
×
924

925
    // Process interleaved (diffs, replace) pairs
926
    for (diffs, replace) in &group.0.values {
×
927
        for diff in diffs {
×
928
            let elem_path = base_path.with(PathSegment::Index(*index));
×
929
            collect_leaf_changes_inner(diff, elem_path, changes);
×
930
            *index += 1;
×
931
        }
×
932
        collect_from_replace_group(base_path, replace, index, changes);
×
933
    }
934

935
    // Process trailing diffs
936
    if let Some(diffs) = &group.0.last {
×
937
        for diff in diffs {
×
938
            let elem_path = base_path.with(PathSegment::Index(*index));
×
939
            collect_leaf_changes_inner(diff, elem_path, changes);
×
940
            *index += 1;
×
941
        }
×
942
    }
×
943
}
×
944

945
fn collect_from_replace_group<'mem, 'facet>(
×
946
    base_path: &Path,
×
947
    group: &crate::ReplaceGroup<'mem, 'facet>,
×
948
    index: &mut usize,
×
949
    changes: &mut Vec<LeafChange<'mem, 'facet>>,
×
950
) {
×
951
    // For replace groups, we have removals and additions
952
    // If counts match, treat as 1:1 replacements at the same index
953
    // Otherwise, show as deletions followed by insertions
954

955
    if group.removals.len() == group.additions.len() {
×
956
        // 1:1 replacements
957
        for (from, to) in group.removals.iter().zip(group.additions.iter()) {
×
958
            let elem_path = base_path.with(PathSegment::Index(*index));
×
959
            // Check if this is actually a nested diff
960
            let nested = diff_new_peek(*from, *to);
×
961
            if matches!(nested, Diff::Replace { .. }) {
×
962
                changes.push(LeafChange {
×
963
                    path: elem_path,
×
964
                    kind: LeafChangeKind::Replace {
×
965
                        from: *from,
×
966
                        to: *to,
×
967
                    },
×
968
                });
×
969
            } else {
×
970
                collect_leaf_changes_inner(&nested, elem_path, changes);
×
971
            }
×
972
            *index += 1;
×
973
        }
974
    } else {
975
        // Mixed deletions and insertions
976
        for from in &group.removals {
×
977
            let elem_path = base_path.with(PathSegment::Index(*index));
×
978
            changes.push(LeafChange {
×
979
                path: elem_path.clone(),
×
980
                kind: LeafChangeKind::Delete { value: *from },
×
981
            });
×
982
            *index += 1;
×
983
        }
×
984
        // Insertions happen at current index
985
        for to in &group.additions {
×
986
            let elem_path = base_path.with(PathSegment::Index(*index));
×
987
            changes.push(LeafChange {
×
988
                path: elem_path,
×
989
                kind: LeafChangeKind::Insert { value: *to },
×
990
            });
×
991
            *index += 1;
×
992
        }
×
993
    }
994
}
×
995

996
/// A single leaf-level change in a diff, with path information.
997
#[derive(Debug, Clone)]
998
pub struct LeafChange<'mem, 'facet> {
999
    /// The path from root to this change
1000
    pub path: Path,
1001
    /// The kind of change
1002
    pub kind: LeafChangeKind<'mem, 'facet>,
1003
}
1004

1005
/// The kind of leaf change.
1006
#[derive(Debug, Clone)]
1007
pub enum LeafChangeKind<'mem, 'facet> {
1008
    /// A value was replaced
1009
    Replace {
1010
        /// The old value
1011
        from: Peek<'mem, 'facet>,
1012
        /// The new value
1013
        to: Peek<'mem, 'facet>,
1014
    },
1015
    /// A value was deleted
1016
    Delete {
1017
        /// The deleted value
1018
        value: Peek<'mem, 'facet>,
1019
    },
1020
    /// A value was inserted
1021
    Insert {
1022
        /// The inserted value
1023
        value: Peek<'mem, 'facet>,
1024
    },
1025
}
1026

1027
impl<'mem, 'facet> LeafChange<'mem, 'facet> {
1028
    /// Format this change without colors.
1029
    pub fn format_plain(&self) -> String {
×
1030
        use facet_pretty::PrettyPrinter;
1031

1032
        let printer = PrettyPrinter::default()
×
1033
            .with_colors(false)
×
1034
            .with_minimal_option_names(true);
×
1035

1036
        let mut out = String::new();
×
1037

1038
        // Show path if non-empty
1039
        if !self.path.0.is_empty() {
×
1040
            out.push_str(&format!("{}: ", self.path));
×
1041
        }
×
1042

1043
        match &self.kind {
×
1044
            LeafChangeKind::Replace { from, to } => {
×
1045
                out.push_str(&format!(
×
1046
                    "{} → {}",
×
1047
                    printer.format_peek(*from),
×
1048
                    printer.format_peek(*to)
×
1049
                ));
×
1050
            }
×
1051
            LeafChangeKind::Delete { value } => {
×
1052
                out.push_str(&format!("- {}", printer.format_peek(*value)));
×
1053
            }
×
1054
            LeafChangeKind::Insert { value } => {
×
1055
                out.push_str(&format!("+ {}", printer.format_peek(*value)));
×
1056
            }
×
1057
        }
1058

1059
        out
×
1060
    }
×
1061

1062
    /// Format this change with colors.
1063
    pub fn format_colored(&self) -> String {
×
1064
        use facet_pretty::{PrettyPrinter, tokyo_night};
1065
        use owo_colors::OwoColorize;
1066

1067
        let printer = PrettyPrinter::default()
×
1068
            .with_colors(false)
×
1069
            .with_minimal_option_names(true);
×
1070

1071
        let mut out = String::new();
×
1072

1073
        // Show path if non-empty (in field name color)
1074
        if !self.path.0.is_empty() {
×
1075
            out.push_str(&format!(
×
1076
                "{}: ",
×
1077
                format!("{}", self.path).color(tokyo_night::FIELD_NAME)
×
1078
            ));
×
1079
        }
×
1080

1081
        match &self.kind {
×
1082
            LeafChangeKind::Replace { from, to } => {
×
1083
                out.push_str(&format!(
×
1084
                    "{} {} {}",
×
1085
                    printer.format_peek(*from).color(tokyo_night::DELETION),
×
1086
                    "→".color(tokyo_night::COMMENT),
×
1087
                    printer.format_peek(*to).color(tokyo_night::INSERTION)
×
1088
                ));
×
1089
            }
×
1090
            LeafChangeKind::Delete { value } => {
×
1091
                out.push_str(&format!(
×
1092
                    "{} {}",
×
1093
                    "-".color(tokyo_night::DELETION),
×
1094
                    printer.format_peek(*value).color(tokyo_night::DELETION)
×
1095
                ));
×
1096
            }
×
1097
            LeafChangeKind::Insert { value } => {
×
1098
                out.push_str(&format!(
×
1099
                    "{} {}",
×
1100
                    "+".color(tokyo_night::INSERTION),
×
1101
                    printer.format_peek(*value).color(tokyo_night::INSERTION)
×
1102
                ));
×
1103
            }
×
1104
        }
1105

1106
        out
×
1107
    }
×
1108
}
1109

1110
impl<'mem, 'facet> std::fmt::Display for LeafChange<'mem, 'facet> {
1111
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
1112
        write!(f, "{}", self.format_plain())
×
1113
    }
×
1114
}
1115

1116
/// Configuration for diff formatting.
1117
#[derive(Debug, Clone)]
1118
pub struct DiffFormat {
1119
    /// Use colors in output
1120
    pub colors: bool,
1121
    /// Maximum number of changes before switching to summary mode
1122
    pub max_inline_changes: usize,
1123
    /// Whether to use compact (path-based) format for few changes
1124
    pub prefer_compact: bool,
1125
}
1126

1127
impl Default for DiffFormat {
1128
    fn default() -> Self {
×
1129
        Self {
×
1130
            colors: true,
×
1131
            max_inline_changes: 10,
×
1132
            prefer_compact: true,
×
1133
        }
×
1134
    }
×
1135
}
1136

1137
/// Format the diff with the given configuration.
1138
///
1139
/// This chooses between compact (path-based) and tree (nested) format
1140
/// based on the number of changes and the configuration.
1141
pub fn format_diff(diff: &Diff<'_, '_>, config: &DiffFormat) -> String {
×
1142
    if matches!(diff, Diff::Equal { .. }) {
×
1143
        return if config.colors {
×
1144
            use facet_pretty::tokyo_night;
1145
            use owo_colors::OwoColorize;
1146
            "(no changes)".color(tokyo_night::MUTED).to_string()
×
1147
        } else {
1148
            "(no changes)".to_string()
×
1149
        };
1150
    }
×
1151

1152
    let changes = collect_leaf_changes(diff);
×
1153

1154
    if changes.is_empty() {
×
1155
        return if config.colors {
×
1156
            use facet_pretty::tokyo_night;
1157
            use owo_colors::OwoColorize;
1158
            "(no changes)".color(tokyo_night::MUTED).to_string()
×
1159
        } else {
1160
            "(no changes)".to_string()
×
1161
        };
1162
    }
×
1163

1164
    // Use compact format if preferred and we have few changes
1165
    if config.prefer_compact && changes.len() <= config.max_inline_changes {
×
1166
        let mut out = String::new();
×
1167
        for (i, change) in changes.iter().enumerate() {
×
1168
            if i > 0 {
×
1169
                out.push('\n');
×
1170
            }
×
1171
            if config.colors {
×
1172
                out.push_str(&change.format_colored());
×
1173
            } else {
×
1174
                out.push_str(&change.format_plain());
×
1175
            }
×
1176
        }
1177
        return out;
×
1178
    }
×
1179

1180
    // Fall back to tree format for many changes
1181
    if changes.len() > config.max_inline_changes {
×
1182
        let mut out = String::new();
×
1183

1184
        // Show first few changes
1185
        for (i, change) in changes.iter().take(config.max_inline_changes).enumerate() {
×
1186
            if i > 0 {
×
1187
                out.push('\n');
×
1188
            }
×
1189
            if config.colors {
×
1190
                out.push_str(&change.format_colored());
×
1191
            } else {
×
1192
                out.push_str(&change.format_plain());
×
1193
            }
×
1194
        }
1195

1196
        // Show summary of remaining
1197
        let remaining = changes.len() - config.max_inline_changes;
×
1198
        if remaining > 0 {
×
1199
            out.push('\n');
×
1200
            let summary = format!(
×
1201
                "... and {} more change{}",
1202
                remaining,
1203
                if remaining == 1 { "" } else { "s" }
×
1204
            );
1205
            if config.colors {
×
1206
                use facet_pretty::tokyo_night;
1207
                use owo_colors::OwoColorize;
1208
                out.push_str(&summary.color(tokyo_night::MUTED).to_string());
×
1209
            } else {
×
1210
                out.push_str(&summary);
×
1211
            }
×
1212
        }
×
1213
        return out;
×
1214
    }
×
1215

1216
    // Default: use Display impl (tree format)
1217
    format!("{diff}")
×
1218
}
×
1219

1220
/// Format the diff with default configuration.
1221
pub fn format_diff_default(diff: &Diff<'_, '_>) -> String {
×
1222
    format_diff(diff, &DiffFormat::default())
×
1223
}
×
1224

1225
/// Format the diff in compact mode (path-based, no tree structure).
1226
pub fn format_diff_compact(diff: &Diff<'_, '_>) -> String {
×
1227
    format_diff(
×
1228
        diff,
×
1229
        &DiffFormat {
×
1230
            prefer_compact: true,
×
1231
            max_inline_changes: usize::MAX,
×
1232
            ..Default::default()
×
1233
        },
×
1234
    )
1235
}
×
1236

1237
/// Format the diff in compact mode without colors.
1238
pub fn format_diff_compact_plain(diff: &Diff<'_, '_>) -> String {
×
1239
    format_diff(
×
1240
        diff,
×
1241
        &DiffFormat {
×
1242
            colors: false,
×
1243
            prefer_compact: true,
×
1244
            max_inline_changes: usize::MAX,
×
1245
        },
×
1246
    )
1247
}
×
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

© 2025 Coveralls, Inc