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

facet-rs / facet / 20102311692

10 Dec 2025 02:37PM UTC coverage: 58.038% (-0.6%) from 58.588%
20102311692

Pull #1220

github

web-flow
Merge f6e92aca5 into eaae4b56d
Pull Request #1220: feat(cinereus): improve tree matching for leaf nodes and filter no-op moves

1583 of 3075 new or added lines in 18 files covered. (51.48%)

111 existing lines in 3 files now uncovered.

28390 of 48916 relevant lines covered (58.04%)

817.51 hits per line

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

47.46
/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};
11

12
use crate::sequences;
13

14
/// Extension trait that provides a [`diff`](FacetDiff::diff) method for `Facet` types
15
pub trait FacetDiff<'f>: Facet<'f> {
16
    /// Computes the difference between two values that implement `Facet`
17
    fn diff<'a, U: Facet<'f>>(&'a self, other: &'a U) -> Diff<'a, 'f>;
18
}
19

20
impl<'f, T: Facet<'f>> FacetDiff<'f> for T {
21
    fn diff<'a, U: Facet<'f>>(&'a self, other: &'a U) -> Diff<'a, 'f> {
217✔
22
        diff_new(self, other)
217✔
23
    }
217✔
24
}
25

26
/// Computes the difference between two values that implement `Facet`
27
pub fn diff_new<'mem, 'facet, T: Facet<'facet>, U: Facet<'facet>>(
217✔
28
    from: &'mem T,
217✔
29
    to: &'mem U,
217✔
30
) -> Diff<'mem, 'facet> {
217✔
31
    diff_new_peek(Peek::new(from), Peek::new(to))
217✔
32
}
217✔
33

34
pub(crate) fn diff_new_peek<'mem, 'facet>(
29,268✔
35
    from: Peek<'mem, 'facet>,
29,268✔
36
    to: Peek<'mem, 'facet>,
29,268✔
37
) -> Diff<'mem, 'facet> {
29,268✔
38
    // Dereference pointers/references to compare the underlying values
39
    let from = deref_if_pointer(from);
29,268✔
40
    let to = deref_if_pointer(to);
29,268✔
41

42
    if from.shape().id == to.shape().id && from.shape().is_partial_eq() && from == to {
29,268✔
43
        return Diff::Equal { value: Some(from) };
17,395✔
44
    }
11,873✔
45

46
    match (
47
        (from.shape().def, from.shape().ty),
11,873✔
48
        (to.shape().def, to.shape().ty),
11,873✔
49
    ) {
50
        ((_, Type::User(UserType::Struct(from_ty))), (_, Type::User(UserType::Struct(to_ty))))
4,544✔
51
            if from_ty.kind == to_ty.kind =>
4,544✔
52
        {
53
            let from_ty = from.into_struct().unwrap();
4,544✔
54
            let to_ty = to.into_struct().unwrap();
4,544✔
55

56
            let value = if [StructKind::Tuple, StructKind::TupleStruct].contains(&from_ty.ty().kind)
4,544✔
57
            {
58
                let from = from_ty.fields().map(|x| x.1).collect();
25✔
59
                let to = to_ty.fields().map(|x| x.1).collect();
25✔
60

61
                let updates = sequences::diff(from, to);
25✔
62

63
                Value::Tuple { updates }
25✔
64
            } else {
65
                let mut updates = HashMap::new();
4,519✔
66
                let mut deletions = HashMap::new();
4,519✔
67
                let mut insertions = HashMap::new();
4,519✔
68
                let mut unchanged = HashSet::new();
4,519✔
69

70
                for (field, from) in from_ty.fields() {
21,605✔
71
                    if let Ok(to) = to_ty.field_by_name(field.name) {
21,605✔
72
                        let diff = diff_new_peek(from, to);
21,604✔
73
                        if diff.is_equal() {
21,604✔
74
                            unchanged.insert(Cow::Borrowed(field.name));
17,104✔
75
                        } else {
17,104✔
76
                            updates.insert(Cow::Borrowed(field.name), diff);
4,500✔
77
                        }
4,500✔
78
                    } else {
1✔
79
                        deletions.insert(Cow::Borrowed(field.name), from);
1✔
80
                    }
1✔
81
                }
82

83
                for (field, to) in to_ty.fields() {
21,605✔
84
                    if from_ty.field_by_name(field.name).is_err() {
21,605✔
85
                        insertions.insert(Cow::Borrowed(field.name), to);
1✔
86
                    }
21,604✔
87
                }
88
                Value::Struct {
4,519✔
89
                    updates,
4,519✔
90
                    deletions,
4,519✔
91
                    insertions,
4,519✔
92
                    unchanged,
4,519✔
93
                }
4,519✔
94
            };
95

96
            Diff::User {
4,544✔
97
                from: from.shape(),
4,544✔
98
                to: to.shape(),
4,544✔
99
                variant: None,
4,544✔
100
                value,
4,544✔
101
            }
4,544✔
102
        }
103
        ((_, Type::User(UserType::Enum(_))), (_, Type::User(UserType::Enum(_)))) => {
104
            let from_enum = from.into_enum().unwrap();
1,147✔
105
            let to_enum = to.into_enum().unwrap();
1,147✔
106

107
            let from_variant = from_enum.active_variant().unwrap();
1,147✔
108
            let to_variant = to_enum.active_variant().unwrap();
1,147✔
109

110
            if from_variant.name != to_variant.name
1,147✔
111
                || from_variant.data.kind != to_variant.data.kind
1,124✔
112
            {
113
                return Diff::Replace { from, to };
23✔
114
            }
1,124✔
115

116
            let value =
1,124✔
117
                if [StructKind::Tuple, StructKind::TupleStruct].contains(&from_variant.data.kind) {
1,124✔
118
                    let from = from_enum.fields().map(|x| x.1).collect();
1,121✔
119
                    let to = to_enum.fields().map(|x| x.1).collect();
1,121✔
120

121
                    let updates = sequences::diff(from, to);
1,121✔
122

123
                    Value::Tuple { updates }
1,121✔
124
                } else {
125
                    let mut updates = HashMap::new();
3✔
126
                    let mut deletions = HashMap::new();
3✔
127
                    let mut insertions = HashMap::new();
3✔
128
                    let mut unchanged = HashSet::new();
3✔
129

130
                    for (field, from) in from_enum.fields() {
3✔
131
                        if let Ok(Some(to)) = to_enum.field_by_name(field.name) {
2✔
132
                            let diff = diff_new_peek(from, to);
2✔
133
                            if diff.is_equal() {
2✔
134
                                unchanged.insert(Cow::Borrowed(field.name));
1✔
135
                            } else {
1✔
136
                                updates.insert(Cow::Borrowed(field.name), diff);
1✔
137
                            }
1✔
138
                        } else {
×
139
                            deletions.insert(Cow::Borrowed(field.name), from);
×
140
                        }
×
141
                    }
142

143
                    for (field, to) in to_enum.fields() {
3✔
144
                        if !from_enum
2✔
145
                            .field_by_name(field.name)
2✔
146
                            .is_ok_and(|x| x.is_some())
2✔
147
                        {
×
148
                            insertions.insert(Cow::Borrowed(field.name), to);
×
149
                        }
2✔
150
                    }
151

152
                    Value::Struct {
3✔
153
                        updates,
3✔
154
                        deletions,
3✔
155
                        insertions,
3✔
156
                        unchanged,
3✔
157
                    }
3✔
158
                };
159

160
            Diff::User {
1,124✔
161
                from: from_enum.shape(),
1,124✔
162
                to: to_enum.shape(),
1,124✔
163
                variant: Some(from_variant.name),
1,124✔
164
                value,
1,124✔
165
            }
1,124✔
166
        }
167
        ((Def::Option(_), _), (Def::Option(_), _)) => {
168
            let from_option = from.into_option().unwrap();
21✔
169
            let to_option = to.into_option().unwrap();
21✔
170

171
            let (Some(from_value), Some(to_value)) = (from_option.value(), to_option.value())
21✔
172
            else {
173
                return Diff::Replace { from, to };
3✔
174
            };
175

176
            // Use sequences::diff to properly handle nested diffs
177
            let updates = sequences::diff(vec![from_value], vec![to_value]);
18✔
178

179
            Diff::User {
18✔
180
                from: from.shape(),
18✔
181
                to: to.shape(),
18✔
182
                variant: Some("Some"),
18✔
183
                value: Value::Tuple { updates },
18✔
184
            }
18✔
185
        }
186
        (
187
            (Def::List(_) | Def::Slice(_), _) | (_, Type::Sequence(_)),
188
            (Def::List(_) | Def::Slice(_), _) | (_, Type::Sequence(_)),
189
        ) => {
190
            let from_list = from.into_list_like().unwrap();
301✔
191
            let to_list = to.into_list_like().unwrap();
301✔
192

193
            let updates = sequences::diff(
301✔
194
                from_list.iter().collect::<Vec<_>>(),
301✔
195
                to_list.iter().collect::<Vec<_>>(),
301✔
196
            );
197

198
            Diff::Sequence {
301✔
199
                from: from.shape(),
301✔
200
                to: to.shape(),
301✔
201
                updates,
301✔
202
            }
301✔
203
        }
204
        ((Def::DynamicValue(_), _), (Def::DynamicValue(_), _)) => diff_dynamic_values(from, to),
163✔
205
        // DynamicValue vs concrete type
206
        ((Def::DynamicValue(_), _), _) => diff_dynamic_vs_concrete(from, to, false),
216✔
207
        (_, (Def::DynamicValue(_), _)) => diff_dynamic_vs_concrete(to, from, true),
77✔
208
        _ => Diff::Replace { from, to },
5,404✔
209
    }
210
}
29,268✔
211

212
/// Diff two dynamic values (like `facet_value::Value`)
213
fn diff_dynamic_values<'mem, 'facet>(
163✔
214
    from: Peek<'mem, 'facet>,
163✔
215
    to: Peek<'mem, 'facet>,
163✔
216
) -> Diff<'mem, 'facet> {
163✔
217
    let from_dyn = from.into_dynamic_value().unwrap();
163✔
218
    let to_dyn = to.into_dynamic_value().unwrap();
163✔
219

220
    let from_kind = from_dyn.kind();
163✔
221
    let to_kind = to_dyn.kind();
163✔
222

223
    // If kinds differ, just return Replace
224
    if from_kind != to_kind {
163✔
225
        return Diff::Replace { from, to };
3✔
226
    }
160✔
227

228
    match from_kind {
160✔
NEW
229
        DynValueKind::Null => Diff::Equal { value: Some(from) },
×
230
        DynValueKind::Bool => {
231
            if from_dyn.as_bool() == to_dyn.as_bool() {
12✔
NEW
232
                Diff::Equal { value: Some(from) }
×
233
            } else {
234
                Diff::Replace { from, to }
12✔
235
            }
236
        }
237
        DynValueKind::Number => {
238
            // Compare numbers - try exact integer comparison first, then float
239
            let same = match (from_dyn.as_i64(), to_dyn.as_i64()) {
67✔
240
                (Some(l), Some(r)) => l == r,
67✔
NEW
241
                _ => match (from_dyn.as_u64(), to_dyn.as_u64()) {
×
NEW
242
                    (Some(l), Some(r)) => l == r,
×
NEW
243
                    _ => match (from_dyn.as_f64(), to_dyn.as_f64()) {
×
NEW
244
                        (Some(l), Some(r)) => l == r,
×
NEW
245
                        _ => false,
×
246
                    },
247
                },
248
            };
249
            if same {
67✔
NEW
250
                Diff::Equal { value: Some(from) }
×
251
            } else {
252
                Diff::Replace { from, to }
67✔
253
            }
254
        }
255
        DynValueKind::String => {
256
            if from_dyn.as_str() == to_dyn.as_str() {
30✔
NEW
257
                Diff::Equal { value: Some(from) }
×
258
            } else {
259
                Diff::Replace { from, to }
30✔
260
            }
261
        }
262
        DynValueKind::Bytes => {
NEW
263
            if from_dyn.as_bytes() == to_dyn.as_bytes() {
×
NEW
264
                Diff::Equal { value: Some(from) }
×
265
            } else {
NEW
266
                Diff::Replace { from, to }
×
267
            }
268
        }
269
        DynValueKind::Array => {
270
            // Use the sequence diff algorithm for arrays
271
            let from_iter = from_dyn.array_iter();
5✔
272
            let to_iter = to_dyn.array_iter();
5✔
273

274
            let from_elems: Vec<_> = from_iter.map(|i| i.collect()).unwrap_or_default();
5✔
275
            let to_elems: Vec<_> = to_iter.map(|i| i.collect()).unwrap_or_default();
5✔
276

277
            let updates = sequences::diff(from_elems, to_elems);
5✔
278

279
            Diff::Sequence {
5✔
280
                from: from.shape(),
5✔
281
                to: to.shape(),
5✔
282
                updates,
5✔
283
            }
5✔
284
        }
285
        DynValueKind::Object => {
286
            // Treat objects like struct diffs
287
            let from_len = from_dyn.object_len().unwrap_or(0);
46✔
288
            let to_len = to_dyn.object_len().unwrap_or(0);
46✔
289

290
            let mut updates = HashMap::new();
46✔
291
            let mut deletions = HashMap::new();
46✔
292
            let mut insertions = HashMap::new();
46✔
293
            let mut unchanged = HashSet::new();
46✔
294

295
            // Collect keys from `from`
296
            let mut from_keys: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
46✔
297
            for i in 0..from_len {
107✔
298
                if let Some((key, value)) = from_dyn.object_get_entry(i) {
107✔
299
                    from_keys.insert(key.to_owned(), value);
107✔
300
                }
107✔
301
            }
302

303
            // Collect keys from `to`
304
            let mut to_keys: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
46✔
305
            for i in 0..to_len {
107✔
306
                if let Some((key, value)) = to_dyn.object_get_entry(i) {
107✔
307
                    to_keys.insert(key.to_owned(), value);
107✔
308
                }
107✔
309
            }
310

311
            // Compare entries
312
            for (key, from_value) in &from_keys {
107✔
313
                if let Some(to_value) = to_keys.get(key) {
107✔
314
                    let diff = diff_new_peek(*from_value, *to_value);
105✔
315
                    if diff.is_equal() {
105✔
316
                        unchanged.insert(Cow::Owned(key.clone()));
34✔
317
                    } else {
71✔
318
                        updates.insert(Cow::Owned(key.clone()), diff);
71✔
319
                    }
71✔
320
                } else {
2✔
321
                    deletions.insert(Cow::Owned(key.clone()), *from_value);
2✔
322
                }
2✔
323
            }
324

325
            for (key, to_value) in &to_keys {
107✔
326
                if !from_keys.contains_key(key) {
107✔
327
                    insertions.insert(Cow::Owned(key.clone()), *to_value);
2✔
328
                }
105✔
329
            }
330

331
            Diff::User {
46✔
332
                from: from.shape(),
46✔
333
                to: to.shape(),
46✔
334
                variant: None,
46✔
335
                value: Value::Struct {
46✔
336
                    updates,
46✔
337
                    deletions,
46✔
338
                    insertions,
46✔
339
                    unchanged,
46✔
340
                },
46✔
341
            }
46✔
342
        }
343
        DynValueKind::DateTime => {
344
            // Compare datetime by their components
NEW
345
            if from_dyn.as_datetime() == to_dyn.as_datetime() {
×
NEW
346
                Diff::Equal { value: Some(from) }
×
347
            } else {
NEW
348
                Diff::Replace { from, to }
×
349
            }
350
        }
351
        DynValueKind::QName | DynValueKind::Uuid => {
352
            // For QName and Uuid, compare by their raw representation
353
            // Since they have the same kind, we can only compare by Replace semantics
NEW
354
            Diff::Replace { from, to }
×
355
        }
356
    }
357
}
163✔
358

359
/// Diff a DynamicValue against a concrete type
360
/// `dyn_peek` is the DynamicValue, `concrete_peek` is the concrete type
361
/// `swapped` indicates if the original from/to were swapped (true means dyn_peek is actually "to")
362
fn diff_dynamic_vs_concrete<'mem, 'facet>(
293✔
363
    dyn_peek: Peek<'mem, 'facet>,
293✔
364
    concrete_peek: Peek<'mem, 'facet>,
293✔
365
    swapped: bool,
293✔
366
) -> Diff<'mem, 'facet> {
293✔
367
    // Determine actual from/to based on swapped flag
368
    let (from_peek, to_peek) = if swapped {
293✔
369
        (concrete_peek, dyn_peek)
77✔
370
    } else {
371
        (dyn_peek, concrete_peek)
216✔
372
    };
373
    let dyn_val = dyn_peek.into_dynamic_value().unwrap();
293✔
374
    let dyn_kind = dyn_val.kind();
293✔
375

376
    // Try to match based on the DynamicValue's kind
377
    match dyn_kind {
293✔
378
        DynValueKind::Bool => {
379
            if concrete_peek
3✔
380
                .get::<bool>()
3✔
381
                .ok()
3✔
382
                .is_some_and(|&v| dyn_val.as_bool() == Some(v))
3✔
383
            {
384
                return Diff::Equal {
2✔
385
                    value: Some(from_peek),
2✔
386
                };
2✔
387
            }
1✔
388
        }
389
        DynValueKind::Number => {
390
            let is_equal =
207✔
391
                // Try signed integers
392
                concrete_peek.get::<i8>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
207✔
393
                || concrete_peek.get::<i16>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
207✔
394
                || concrete_peek.get::<i32>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
207✔
395
                || concrete_peek.get::<i64>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v))
205✔
396
                || concrete_peek.get::<isize>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
147✔
397
                // Try unsigned integers
398
                || concrete_peek.get::<u8>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
147✔
399
                || concrete_peek.get::<u16>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
137✔
400
                || concrete_peek.get::<u32>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
137✔
401
                || concrete_peek.get::<u64>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v))
137✔
402
                || concrete_peek.get::<usize>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
137✔
403
                // Try floats
404
                || concrete_peek.get::<f32>().ok().is_some_and(|&v| dyn_val.as_f64() == Some(v as f64))
137✔
405
                || concrete_peek.get::<f64>().ok().is_some_and(|&v| dyn_val.as_f64() == Some(v));
137✔
406
            if is_equal {
207✔
407
                return Diff::Equal {
71✔
408
                    value: Some(from_peek),
71✔
409
                };
71✔
410
            }
136✔
411
        }
412
        DynValueKind::String => {
413
            if concrete_peek
37✔
414
                .as_str()
37✔
415
                .is_some_and(|s| dyn_val.as_str() == Some(s))
37✔
416
            {
417
                return Diff::Equal {
19✔
418
                    value: Some(from_peek),
19✔
419
                };
19✔
420
            }
18✔
421
        }
422
        DynValueKind::Array => {
423
            // Try to diff as sequences if the concrete type is list-like
424
            if let Ok(concrete_list) = concrete_peek.into_list_like() {
14✔
425
                let dyn_elems: Vec<_> = dyn_val
12✔
426
                    .array_iter()
12✔
427
                    .map(|i| i.collect())
12✔
428
                    .unwrap_or_default();
12✔
429
                let concrete_elems: Vec<_> = concrete_list.iter().collect();
12✔
430

431
                // Use correct order based on swapped flag
432
                let (from_elems, to_elems) = if swapped {
12✔
433
                    (concrete_elems, dyn_elems)
4✔
434
                } else {
435
                    (dyn_elems, concrete_elems)
8✔
436
                };
437
                let updates = sequences::diff(from_elems, to_elems);
12✔
438

439
                return Diff::Sequence {
12✔
440
                    from: from_peek.shape(),
12✔
441
                    to: to_peek.shape(),
12✔
442
                    updates,
12✔
443
                };
12✔
444
            }
2✔
445
        }
446
        DynValueKind::Object => {
447
            // Try to diff as struct if the concrete type is a struct
448
            if let Ok(concrete_struct) = concrete_peek.into_struct() {
31✔
449
                let dyn_len = dyn_val.object_len().unwrap_or(0);
31✔
450

451
                let mut updates = HashMap::new();
31✔
452
                let mut deletions = HashMap::new();
31✔
453
                let mut insertions = HashMap::new();
31✔
454
                let mut unchanged = HashSet::new();
31✔
455

456
                // Collect keys from dynamic object
457
                let mut dyn_keys: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
31✔
458
                for i in 0..dyn_len {
64✔
459
                    if let Some((key, value)) = dyn_val.object_get_entry(i) {
64✔
460
                        dyn_keys.insert(key.to_owned(), value);
64✔
461
                    }
64✔
462
                }
463

464
                // Compare with concrete struct fields
465
                // When swapped, dyn is "to" and concrete is "from", so we need to swap the diff direction
466
                for (key, dyn_value) in &dyn_keys {
64✔
467
                    if let Ok(concrete_value) = concrete_struct.field_by_name(key) {
64✔
468
                        let diff = if swapped {
64✔
469
                            diff_new_peek(concrete_value, *dyn_value)
2✔
470
                        } else {
471
                            diff_new_peek(*dyn_value, concrete_value)
62✔
472
                        };
473
                        if diff.is_equal() {
64✔
474
                            unchanged.insert(Cow::Owned(key.clone()));
27✔
475
                        } else {
37✔
476
                            updates.insert(Cow::Owned(key.clone()), diff);
37✔
477
                        }
37✔
478
                    } else {
479
                        // Field in dyn but not in concrete
480
                        // If swapped: dyn is "to", so this is an insertion
481
                        // If not swapped: dyn is "from", so this is a deletion
NEW
482
                        if swapped {
×
NEW
483
                            insertions.insert(Cow::Owned(key.clone()), *dyn_value);
×
NEW
484
                        } else {
×
NEW
485
                            deletions.insert(Cow::Owned(key.clone()), *dyn_value);
×
NEW
486
                        }
×
487
                    }
488
                }
489

490
                for (field, concrete_value) in concrete_struct.fields() {
64✔
491
                    if !dyn_keys.contains_key(field.name) {
64✔
492
                        // Field in concrete but not in dyn
493
                        // If swapped: concrete is "from", so this is a deletion
494
                        // If not swapped: concrete is "to", so this is an insertion
NEW
495
                        if swapped {
×
NEW
496
                            deletions.insert(Cow::Borrowed(field.name), concrete_value);
×
NEW
497
                        } else {
×
NEW
498
                            insertions.insert(Cow::Borrowed(field.name), concrete_value);
×
NEW
499
                        }
×
500
                    }
64✔
501
                }
502

503
                return Diff::User {
31✔
504
                    from: from_peek.shape(),
31✔
505
                    to: to_peek.shape(),
31✔
506
                    variant: None,
31✔
507
                    value: Value::Struct {
31✔
508
                        updates,
31✔
509
                        deletions,
31✔
510
                        insertions,
31✔
511
                        unchanged,
31✔
512
                    },
31✔
513
                };
31✔
UNCOV
514
            }
×
515
        }
516
        // For other kinds (Null, Bytes, DateTime), fall through to Replace
517
        _ => {}
1✔
518
    }
519

520
    Diff::Replace {
158✔
521
        from: from_peek,
158✔
522
        to: to_peek,
158✔
523
    }
158✔
524
}
293✔
525

526
/// Dereference a pointer/reference to get the underlying value
527
fn deref_if_pointer<'mem, 'facet>(peek: Peek<'mem, 'facet>) -> Peek<'mem, 'facet> {
58,628✔
528
    if let Ok(ptr) = peek.into_pointer()
58,628✔
529
        && let Some(target) = ptr.borrow_inner()
92✔
530
    {
531
        return deref_if_pointer(target);
92✔
532
    }
58,536✔
533
    peek
58,536✔
534
}
58,628✔
535

536
pub(crate) fn diff_closeness(diff: &Diff<'_, '_>) -> usize {
1,668✔
537
    match diff {
1,668✔
538
        Diff::Equal { .. } => 1, // This does not actually matter for flattening sequence diffs, because all diffs there are non-equal
16✔
539
        Diff::Replace { .. } => 0,
216✔
NEW
540
        Diff::Sequence { updates, .. } => updates.closeness(),
×
541
        Diff::User {
542
            from, to, value, ..
1,436✔
543
        } => value.closeness() + (from == to) as usize,
1,436✔
544
    }
545
}
1,668✔
546

547
/// Collect all leaf-level changes with their paths.
548
///
549
/// This walks the diff tree recursively and collects every terminal change
550
/// (scalar replacements) along with the path to reach them. This is useful
551
/// for compact display: if there's only one leaf change deep in a tree,
552
/// you can show `path.to.field: old → new` instead of nested structure.
NEW
553
pub fn collect_leaf_changes<'mem, 'facet>(
×
NEW
554
    diff: &Diff<'mem, 'facet>,
×
NEW
555
) -> Vec<LeafChange<'mem, 'facet>> {
×
NEW
556
    let mut changes = Vec::new();
×
NEW
557
    collect_leaf_changes_inner(diff, Path::new(), &mut changes);
×
NEW
558
    changes
×
NEW
559
}
×
560

NEW
561
fn collect_leaf_changes_inner<'mem, 'facet>(
×
NEW
562
    diff: &Diff<'mem, 'facet>,
×
NEW
563
    path: Path,
×
NEW
564
    changes: &mut Vec<LeafChange<'mem, 'facet>>,
×
NEW
565
) {
×
NEW
566
    match diff {
×
NEW
567
        Diff::Equal { .. } => {
×
NEW
568
            // No change
×
NEW
569
        }
×
NEW
570
        Diff::Replace { from, to } => {
×
NEW
571
            // This is a leaf change
×
NEW
572
            changes.push(LeafChange {
×
NEW
573
                path,
×
NEW
574
                kind: LeafChangeKind::Replace {
×
NEW
575
                    from: *from,
×
NEW
576
                    to: *to,
×
NEW
577
                },
×
NEW
578
            });
×
NEW
579
        }
×
580
        Diff::User {
NEW
581
            value,
×
NEW
582
            variant,
×
NEW
583
            from,
×
584
            ..
585
        } => {
586
            // For Option::Some, skip the variant in the path since it's implied
587
            // (the value exists, so it's Some)
NEW
588
            let is_option = matches!(from.def, Def::Option(_));
×
589

NEW
590
            let base_path = if let Some(v) = variant {
×
NEW
591
                if is_option && *v == "Some" {
×
NEW
592
                    path // Skip "::Some" for options
×
593
                } else {
NEW
594
                    path.with(PathSegment::Variant(Cow::Borrowed(*v)))
×
595
                }
596
            } else {
NEW
597
                path
×
598
            };
599

NEW
600
            match value {
×
601
                Value::Struct {
NEW
602
                    updates,
×
NEW
603
                    deletions,
×
NEW
604
                    insertions,
×
605
                    ..
606
                } => {
607
                    // Recurse into field updates
NEW
608
                    for (field, diff) in updates {
×
NEW
609
                        let field_path = base_path.with(PathSegment::Field(field.clone()));
×
NEW
610
                        collect_leaf_changes_inner(diff, field_path, changes);
×
NEW
611
                    }
×
612
                    // Deletions are leaf changes
NEW
613
                    for (field, peek) in deletions {
×
NEW
614
                        let field_path = base_path.with(PathSegment::Field(field.clone()));
×
NEW
615
                        changes.push(LeafChange {
×
NEW
616
                            path: field_path,
×
NEW
617
                            kind: LeafChangeKind::Delete { value: *peek },
×
NEW
618
                        });
×
NEW
619
                    }
×
620
                    // Insertions are leaf changes
NEW
621
                    for (field, peek) in insertions {
×
NEW
622
                        let field_path = base_path.with(PathSegment::Field(field.clone()));
×
NEW
623
                        changes.push(LeafChange {
×
NEW
624
                            path: field_path,
×
NEW
625
                            kind: LeafChangeKind::Insert { value: *peek },
×
NEW
626
                        });
×
NEW
627
                    }
×
628
                }
NEW
629
                Value::Tuple { updates } => {
×
630
                    // For single-element tuples (like Option::Some), skip the index
NEW
631
                    if is_option {
×
NEW
632
                        // Recurse directly without adding [0]
×
NEW
633
                        collect_from_updates_for_single_elem(&base_path, updates, changes);
×
NEW
634
                    } else {
×
NEW
635
                        collect_from_updates(&base_path, updates, changes);
×
NEW
636
                    }
×
637
                }
638
            }
639
        }
NEW
640
        Diff::Sequence { updates, .. } => {
×
NEW
641
            collect_from_updates(&path, updates, changes);
×
UNCOV
642
        }
×
643
    }
NEW
644
}
×
645

646
/// Special handling for single-element tuples (like Option::Some)
647
/// where we want to skip the [0] index in the path.
NEW
648
fn collect_from_updates_for_single_elem<'mem, 'facet>(
×
NEW
649
    base_path: &Path,
×
NEW
650
    updates: &Updates<'mem, 'facet>,
×
NEW
651
    changes: &mut Vec<LeafChange<'mem, 'facet>>,
×
NEW
652
) {
×
653
    // For single-element tuples, we expect exactly one change
654
    // Just use base_path directly instead of adding [0]
NEW
655
    if let Some(update_group) = &updates.0.first {
×
656
        // Process the first replace group if present
NEW
657
        if let Some(replace) = &update_group.0.first
×
NEW
658
            && replace.removals.len() == 1
×
NEW
659
            && replace.additions.len() == 1
×
660
        {
NEW
661
            let from = replace.removals[0];
×
NEW
662
            let to = replace.additions[0];
×
NEW
663
            let nested = diff_new_peek(from, to);
×
NEW
664
            if matches!(nested, Diff::Replace { .. }) {
×
NEW
665
                changes.push(LeafChange {
×
NEW
666
                    path: base_path.clone(),
×
NEW
667
                    kind: LeafChangeKind::Replace { from, to },
×
NEW
668
                });
×
NEW
669
            } else {
×
NEW
670
                collect_leaf_changes_inner(&nested, base_path.clone(), changes);
×
UNCOV
671
            }
×
NEW
672
            return;
×
NEW
673
        }
×
674
        // Handle nested diffs
NEW
675
        if let Some(diffs) = &update_group.0.last {
×
NEW
676
            for diff in diffs {
×
NEW
677
                collect_leaf_changes_inner(diff, base_path.clone(), changes);
×
UNCOV
678
            }
×
NEW
679
            return;
×
NEW
680
        }
×
NEW
681
    }
×
682
    // Fallback: use regular handling
NEW
683
    collect_from_updates(base_path, updates, changes);
×
NEW
684
}
×
685

NEW
686
fn collect_from_updates<'mem, 'facet>(
×
NEW
687
    base_path: &Path,
×
NEW
688
    updates: &Updates<'mem, 'facet>,
×
NEW
689
    changes: &mut Vec<LeafChange<'mem, 'facet>>,
×
NEW
690
) {
×
691
    // Walk through the interspersed structure to collect changes with correct indices
NEW
692
    let mut index = 0;
×
693

694
    // Process first update group if present
NEW
695
    if let Some(update_group) = &updates.0.first {
×
NEW
696
        collect_from_update_group(base_path, update_group, &mut index, changes);
×
NEW
697
    }
×
698

699
    // Process interleaved (unchanged, update) pairs
NEW
700
    for (unchanged, update_group) in &updates.0.values {
×
NEW
701
        index += unchanged.len();
×
NEW
702
        collect_from_update_group(base_path, update_group, &mut index, changes);
×
NEW
703
    }
×
704

705
    // Trailing unchanged items don't add changes
NEW
706
}
×
707

NEW
708
fn collect_from_update_group<'mem, 'facet>(
×
NEW
709
    base_path: &Path,
×
NEW
710
    group: &crate::UpdatesGroup<'mem, 'facet>,
×
NEW
711
    index: &mut usize,
×
NEW
712
    changes: &mut Vec<LeafChange<'mem, 'facet>>,
×
NEW
713
) {
×
714
    // Process first replace group if present
NEW
715
    if let Some(replace) = &group.0.first {
×
NEW
716
        collect_from_replace_group(base_path, replace, index, changes);
×
NEW
717
    }
×
718

719
    // Process interleaved (diffs, replace) pairs
NEW
720
    for (diffs, replace) in &group.0.values {
×
NEW
721
        for diff in diffs {
×
NEW
722
            let elem_path = base_path.with(PathSegment::Index(*index));
×
NEW
723
            collect_leaf_changes_inner(diff, elem_path, changes);
×
NEW
724
            *index += 1;
×
NEW
725
        }
×
NEW
726
        collect_from_replace_group(base_path, replace, index, changes);
×
727
    }
728

729
    // Process trailing diffs
NEW
730
    if let Some(diffs) = &group.0.last {
×
NEW
731
        for diff in diffs {
×
NEW
732
            let elem_path = base_path.with(PathSegment::Index(*index));
×
NEW
733
            collect_leaf_changes_inner(diff, elem_path, changes);
×
NEW
734
            *index += 1;
×
NEW
735
        }
×
NEW
736
    }
×
NEW
737
}
×
738

NEW
739
fn collect_from_replace_group<'mem, 'facet>(
×
NEW
740
    base_path: &Path,
×
NEW
741
    group: &crate::ReplaceGroup<'mem, 'facet>,
×
NEW
742
    index: &mut usize,
×
NEW
743
    changes: &mut Vec<LeafChange<'mem, 'facet>>,
×
NEW
744
) {
×
745
    // For replace groups, we have removals and additions
746
    // If counts match, treat as 1:1 replacements at the same index
747
    // Otherwise, show as deletions followed by insertions
748

NEW
749
    if group.removals.len() == group.additions.len() {
×
750
        // 1:1 replacements
NEW
751
        for (from, to) in group.removals.iter().zip(group.additions.iter()) {
×
NEW
752
            let elem_path = base_path.with(PathSegment::Index(*index));
×
753
            // Check if this is actually a nested diff
NEW
754
            let nested = diff_new_peek(*from, *to);
×
NEW
755
            if matches!(nested, Diff::Replace { .. }) {
×
NEW
756
                changes.push(LeafChange {
×
NEW
757
                    path: elem_path,
×
NEW
758
                    kind: LeafChangeKind::Replace {
×
NEW
759
                        from: *from,
×
NEW
760
                        to: *to,
×
NEW
761
                    },
×
NEW
762
                });
×
NEW
763
            } else {
×
NEW
764
                collect_leaf_changes_inner(&nested, elem_path, changes);
×
UNCOV
765
            }
×
NEW
766
            *index += 1;
×
767
        }
768
    } else {
769
        // Mixed deletions and insertions
NEW
770
        for from in &group.removals {
×
NEW
771
            let elem_path = base_path.with(PathSegment::Index(*index));
×
NEW
772
            changes.push(LeafChange {
×
NEW
773
                path: elem_path.clone(),
×
NEW
774
                kind: LeafChangeKind::Delete { value: *from },
×
NEW
775
            });
×
NEW
776
            *index += 1;
×
NEW
777
        }
×
778
        // Insertions happen at current index
NEW
779
        for to in &group.additions {
×
NEW
780
            let elem_path = base_path.with(PathSegment::Index(*index));
×
NEW
781
            changes.push(LeafChange {
×
NEW
782
                path: elem_path,
×
NEW
783
                kind: LeafChangeKind::Insert { value: *to },
×
NEW
784
            });
×
NEW
785
            *index += 1;
×
NEW
786
        }
×
787
    }
NEW
788
}
×
789

790
/// A single leaf-level change in a diff, with path information.
791
#[derive(Debug, Clone)]
792
pub struct LeafChange<'mem, 'facet> {
793
    /// The path from root to this change
794
    pub path: Path,
795
    /// The kind of change
796
    pub kind: LeafChangeKind<'mem, 'facet>,
797
}
798

799
/// The kind of leaf change.
800
#[derive(Debug, Clone)]
801
pub enum LeafChangeKind<'mem, 'facet> {
802
    /// A value was replaced
803
    Replace {
804
        /// The old value
805
        from: Peek<'mem, 'facet>,
806
        /// The new value
807
        to: Peek<'mem, 'facet>,
808
    },
809
    /// A value was deleted
810
    Delete {
811
        /// The deleted value
812
        value: Peek<'mem, 'facet>,
813
    },
814
    /// A value was inserted
815
    Insert {
816
        /// The inserted value
817
        value: Peek<'mem, 'facet>,
818
    },
819
}
820

821
impl<'mem, 'facet> LeafChange<'mem, 'facet> {
822
    /// Format this change without colors.
NEW
823
    pub fn format_plain(&self) -> String {
×
824
        use facet_pretty::PrettyPrinter;
825

NEW
826
        let printer = PrettyPrinter::default()
×
NEW
827
            .with_colors(false)
×
NEW
828
            .with_minimal_option_names(true);
×
829

NEW
830
        let mut out = String::new();
×
831

832
        // Show path if non-empty
NEW
833
        if !self.path.0.is_empty() {
×
NEW
834
            out.push_str(&format!("{}: ", self.path));
×
NEW
835
        }
×
836

NEW
837
        match &self.kind {
×
NEW
838
            LeafChangeKind::Replace { from, to } => {
×
NEW
839
                out.push_str(&format!(
×
NEW
840
                    "{} → {}",
×
NEW
841
                    printer.format_peek(*from),
×
NEW
842
                    printer.format_peek(*to)
×
NEW
843
                ));
×
NEW
844
            }
×
NEW
845
            LeafChangeKind::Delete { value } => {
×
NEW
846
                out.push_str(&format!("- {}", printer.format_peek(*value)));
×
UNCOV
847
            }
×
NEW
848
            LeafChangeKind::Insert { value } => {
×
NEW
849
                out.push_str(&format!("+ {}", printer.format_peek(*value)));
×
NEW
850
            }
×
851
        }
852

NEW
853
        out
×
NEW
854
    }
×
855

856
    /// Format this change with colors.
NEW
857
    pub fn format_colored(&self) -> String {
×
858
        use facet_pretty::{PrettyPrinter, tokyo_night};
859
        use owo_colors::OwoColorize;
860

NEW
861
        let printer = PrettyPrinter::default()
×
NEW
862
            .with_colors(false)
×
NEW
863
            .with_minimal_option_names(true);
×
864

NEW
865
        let mut out = String::new();
×
866

867
        // Show path if non-empty (in field name color)
NEW
868
        if !self.path.0.is_empty() {
×
NEW
869
            out.push_str(&format!(
×
NEW
870
                "{}: ",
×
NEW
871
                format!("{}", self.path).color(tokyo_night::FIELD_NAME)
×
NEW
872
            ));
×
NEW
873
        }
×
874

NEW
875
        match &self.kind {
×
NEW
876
            LeafChangeKind::Replace { from, to } => {
×
NEW
877
                out.push_str(&format!(
×
NEW
878
                    "{} {} {}",
×
NEW
879
                    printer.format_peek(*from).color(tokyo_night::DELETION),
×
NEW
880
                    "→".color(tokyo_night::COMMENT),
×
NEW
881
                    printer.format_peek(*to).color(tokyo_night::INSERTION)
×
NEW
882
                ));
×
NEW
883
            }
×
NEW
884
            LeafChangeKind::Delete { value } => {
×
NEW
885
                out.push_str(&format!(
×
NEW
886
                    "{} {}",
×
NEW
887
                    "-".color(tokyo_night::DELETION),
×
NEW
888
                    printer.format_peek(*value).color(tokyo_night::DELETION)
×
NEW
889
                ));
×
NEW
890
            }
×
NEW
891
            LeafChangeKind::Insert { value } => {
×
NEW
892
                out.push_str(&format!(
×
NEW
893
                    "{} {}",
×
NEW
894
                    "+".color(tokyo_night::INSERTION),
×
NEW
895
                    printer.format_peek(*value).color(tokyo_night::INSERTION)
×
NEW
896
                ));
×
UNCOV
897
            }
×
898
        }
899

NEW
900
        out
×
NEW
901
    }
×
902
}
903

904
impl<'mem, 'facet> std::fmt::Display for LeafChange<'mem, 'facet> {
NEW
905
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
NEW
906
        write!(f, "{}", self.format_plain())
×
NEW
907
    }
×
908
}
909

910
/// Configuration for diff formatting.
911
#[derive(Debug, Clone)]
912
pub struct DiffFormat {
913
    /// Use colors in output
914
    pub colors: bool,
915
    /// Maximum number of changes before switching to summary mode
916
    pub max_inline_changes: usize,
917
    /// Whether to use compact (path-based) format for few changes
918
    pub prefer_compact: bool,
919
}
920

921
impl Default for DiffFormat {
NEW
922
    fn default() -> Self {
×
NEW
923
        Self {
×
NEW
924
            colors: true,
×
NEW
925
            max_inline_changes: 10,
×
NEW
926
            prefer_compact: true,
×
UNCOV
927
        }
×
UNCOV
928
    }
×
929
}
930

931
/// Format the diff with the given configuration.
932
///
933
/// This chooses between compact (path-based) and tree (nested) format
934
/// based on the number of changes and the configuration.
NEW
935
pub fn format_diff(diff: &Diff<'_, '_>, config: &DiffFormat) -> String {
×
NEW
936
    if matches!(diff, Diff::Equal { .. }) {
×
NEW
937
        return if config.colors {
×
938
            use facet_pretty::tokyo_night;
939
            use owo_colors::OwoColorize;
NEW
940
            "(no changes)".color(tokyo_night::MUTED).to_string()
×
941
        } else {
NEW
942
            "(no changes)".to_string()
×
943
        };
NEW
944
    }
×
945

NEW
946
    let changes = collect_leaf_changes(diff);
×
947

NEW
948
    if changes.is_empty() {
×
NEW
949
        return if config.colors {
×
950
            use facet_pretty::tokyo_night;
951
            use owo_colors::OwoColorize;
NEW
952
            "(no changes)".color(tokyo_night::MUTED).to_string()
×
953
        } else {
NEW
954
            "(no changes)".to_string()
×
955
        };
NEW
956
    }
×
957

958
    // Use compact format if preferred and we have few changes
NEW
959
    if config.prefer_compact && changes.len() <= config.max_inline_changes {
×
NEW
960
        let mut out = String::new();
×
NEW
961
        for (i, change) in changes.iter().enumerate() {
×
NEW
962
            if i > 0 {
×
NEW
963
                out.push('\n');
×
NEW
964
            }
×
NEW
965
            if config.colors {
×
NEW
966
                out.push_str(&change.format_colored());
×
NEW
967
            } else {
×
NEW
968
                out.push_str(&change.format_plain());
×
NEW
969
            }
×
970
        }
NEW
971
        return out;
×
UNCOV
972
    }
×
973

974
    // Fall back to tree format for many changes
NEW
975
    if changes.len() > config.max_inline_changes {
×
NEW
976
        let mut out = String::new();
×
977

978
        // Show first few changes
NEW
979
        for (i, change) in changes.iter().take(config.max_inline_changes).enumerate() {
×
NEW
980
            if i > 0 {
×
NEW
981
                out.push('\n');
×
NEW
982
            }
×
NEW
983
            if config.colors {
×
NEW
984
                out.push_str(&change.format_colored());
×
NEW
985
            } else {
×
NEW
986
                out.push_str(&change.format_plain());
×
NEW
987
            }
×
988
        }
989

990
        // Show summary of remaining
NEW
991
        let remaining = changes.len() - config.max_inline_changes;
×
NEW
992
        if remaining > 0 {
×
NEW
993
            out.push('\n');
×
NEW
994
            let summary = format!(
×
995
                "... and {} more change{}",
996
                remaining,
NEW
997
                if remaining == 1 { "" } else { "s" }
×
998
            );
NEW
999
            if config.colors {
×
1000
                use facet_pretty::tokyo_night;
1001
                use owo_colors::OwoColorize;
NEW
1002
                out.push_str(&summary.color(tokyo_night::MUTED).to_string());
×
NEW
1003
            } else {
×
NEW
1004
                out.push_str(&summary);
×
NEW
1005
            }
×
NEW
1006
        }
×
NEW
1007
        return out;
×
UNCOV
1008
    }
×
1009

1010
    // Default: use Display impl (tree format)
NEW
1011
    format!("{diff}")
×
NEW
1012
}
×
1013

1014
/// Format the diff with default configuration.
NEW
1015
pub fn format_diff_default(diff: &Diff<'_, '_>) -> String {
×
NEW
1016
    format_diff(diff, &DiffFormat::default())
×
NEW
1017
}
×
1018

1019
/// Format the diff in compact mode (path-based, no tree structure).
NEW
1020
pub fn format_diff_compact(diff: &Diff<'_, '_>) -> String {
×
NEW
1021
    format_diff(
×
NEW
1022
        diff,
×
NEW
1023
        &DiffFormat {
×
NEW
1024
            prefer_compact: true,
×
NEW
1025
            max_inline_changes: usize::MAX,
×
NEW
1026
            ..Default::default()
×
NEW
1027
        },
×
1028
    )
NEW
1029
}
×
1030

1031
/// Format the diff in compact mode without colors.
NEW
1032
pub fn format_diff_compact_plain(diff: &Diff<'_, '_>) -> String {
×
NEW
1033
    format_diff(
×
NEW
1034
        diff,
×
NEW
1035
        &DiffFormat {
×
NEW
1036
            colors: false,
×
NEW
1037
            prefer_compact: true,
×
NEW
1038
            max_inline_changes: usize::MAX,
×
NEW
1039
        },
×
1040
    )
UNCOV
1041
}
×
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