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

facet-rs / facet / 20111913541

10 Dec 2025 08:11PM UTC coverage: 57.688% (-0.04%) from 57.724%
20111913541

push

github

fasterthanlime
mh

28366 of 49171 relevant lines covered (57.69%)

6269.04 hits per line

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

49.44
/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> {
260✔
22
        diff_new(self, other)
260✔
23
    }
260✔
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>>(
260✔
28
    from: &'mem T,
260✔
29
    to: &'mem U,
260✔
30
) -> Diff<'mem, 'facet> {
260✔
31
    diff_new_peek(Peek::new(from), Peek::new(to))
260✔
32
}
260✔
33

34
/// Computes the difference between two `Peek` values
35
pub fn diff_new_peek<'mem, 'facet>(
574,061✔
36
    from: Peek<'mem, 'facet>,
574,061✔
37
    to: Peek<'mem, 'facet>,
574,061✔
38
) -> Diff<'mem, 'facet> {
574,061✔
39
    // Dereference pointers/references to compare the underlying values
40
    let from = deref_if_pointer(from);
574,061✔
41
    let to = deref_if_pointer(to);
574,061✔
42

43
    // Check for equality if both shapes have the same type_identifier and implement PartialEq
44
    // This handles cases where shapes are structurally equivalent but have different IDs
45
    // (e.g., after deserialization)
46
    let same_type = from.shape().type_identifier == to.shape().type_identifier;
574,061✔
47
    let from_has_partialeq = from.shape().is_partial_eq();
574,061✔
48
    let to_has_partialeq = to.shape().is_partial_eq();
574,061✔
49
    let values_equal = from == to;
574,061✔
50

51
    // log::trace!(
52
    //     "diff_new_peek: type={} same_type={} from_has_partialeq={} to_has_partialeq={} values_equal={}",
53
    //     from.shape().type_identifier,
54
    //     same_type,
55
    //     from_has_partialeq,
56
    //     to_has_partialeq,
57
    //     values_equal
58
    // );
59

60
    if same_type && from_has_partialeq && to_has_partialeq && values_equal {
574,061✔
61
        return Diff::Equal { value: Some(from) };
192,432✔
62
    }
381,629✔
63

64
    match (
65
        (from.shape().def, from.shape().ty),
381,629✔
66
        (to.shape().def, to.shape().ty),
381,629✔
67
    ) {
68
        ((_, Type::User(UserType::Struct(from_ty))), (_, Type::User(UserType::Struct(to_ty))))
189,610✔
69
            if from_ty.kind == to_ty.kind =>
189,610✔
70
        {
71
            let from_ty = from.into_struct().unwrap();
189,610✔
72
            let to_ty = to.into_struct().unwrap();
189,610✔
73

74
            let value = if [StructKind::Tuple, StructKind::TupleStruct].contains(&from_ty.ty().kind)
189,610✔
75
            {
76
                let from = from_ty.fields().map(|x| x.1).collect();
17✔
77
                let to = to_ty.fields().map(|x| x.1).collect();
17✔
78

79
                let updates = sequences::diff(from, to);
17✔
80

81
                Value::Tuple { updates }
17✔
82
            } else {
83
                let mut updates = HashMap::new();
189,593✔
84
                let mut deletions = HashMap::new();
189,593✔
85
                let mut insertions = HashMap::new();
189,593✔
86
                let mut unchanged = HashSet::new();
189,593✔
87

88
                for (field, from) in from_ty.fields() {
382,391✔
89
                    if let Ok(to) = to_ty.field_by_name(field.name) {
382,391✔
90
                        let diff = diff_new_peek(from, to);
382,390✔
91
                        if diff.is_equal() {
382,390✔
92
                            unchanged.insert(Cow::Borrowed(field.name));
381,318✔
93
                        } else {
381,318✔
94
                            updates.insert(Cow::Borrowed(field.name), diff);
1,072✔
95
                        }
1,072✔
96
                    } else {
1✔
97
                        deletions.insert(Cow::Borrowed(field.name), from);
1✔
98
                    }
1✔
99
                }
100

101
                for (field, to) in to_ty.fields() {
382,391✔
102
                    if from_ty.field_by_name(field.name).is_err() {
382,391✔
103
                        insertions.insert(Cow::Borrowed(field.name), to);
1✔
104
                    }
382,390✔
105
                }
106
                Value::Struct {
189,593✔
107
                    updates,
189,593✔
108
                    deletions,
189,593✔
109
                    insertions,
189,593✔
110
                    unchanged,
189,593✔
111
                }
189,593✔
112
            };
113

114
            // If there are no changes, return Equal instead of User
115
            let is_empty = match &value {
189,610✔
116
                Value::Tuple { updates } => updates.is_empty(),
17✔
117
                Value::Struct {
118
                    updates,
189,593✔
119
                    deletions,
189,593✔
120
                    insertions,
189,593✔
121
                    ..
122
                } => updates.is_empty() && deletions.is_empty() && insertions.is_empty(),
189,593✔
123
            };
124
            if is_empty {
189,610✔
125
                return Diff::Equal { value: Some(from) };
188,557✔
126
            }
1,053✔
127

128
            Diff::User {
1,053✔
129
                from: from.shape(),
1,053✔
130
                to: to.shape(),
1,053✔
131
                variant: None,
1,053✔
132
                value,
1,053✔
133
            }
1,053✔
134
        }
135
        ((_, Type::User(UserType::Enum(_))), (_, Type::User(UserType::Enum(_)))) => {
136
            let from_enum = from.into_enum().unwrap();
910✔
137
            let to_enum = to.into_enum().unwrap();
910✔
138

139
            let from_variant = from_enum.active_variant().unwrap();
910✔
140
            let to_variant = to_enum.active_variant().unwrap();
910✔
141

142
            if from_variant.name != to_variant.name
910✔
143
                || from_variant.data.kind != to_variant.data.kind
892✔
144
            {
145
                return Diff::Replace { from, to };
18✔
146
            }
892✔
147

148
            let value =
892✔
149
                if [StructKind::Tuple, StructKind::TupleStruct].contains(&from_variant.data.kind) {
892✔
150
                    let from = from_enum.fields().map(|x| x.1).collect();
598✔
151
                    let to = to_enum.fields().map(|x| x.1).collect();
598✔
152

153
                    let updates = sequences::diff(from, to);
598✔
154

155
                    Value::Tuple { updates }
598✔
156
                } else {
157
                    let mut updates = HashMap::new();
294✔
158
                    let mut deletions = HashMap::new();
294✔
159
                    let mut insertions = HashMap::new();
294✔
160
                    let mut unchanged = HashSet::new();
294✔
161

162
                    for (field, from) in from_enum.fields() {
294✔
163
                        if let Ok(Some(to)) = to_enum.field_by_name(field.name) {
7✔
164
                            let diff = diff_new_peek(from, to);
7✔
165
                            if diff.is_equal() {
7✔
166
                                unchanged.insert(Cow::Borrowed(field.name));
6✔
167
                            } else {
6✔
168
                                updates.insert(Cow::Borrowed(field.name), diff);
1✔
169
                            }
1✔
170
                        } else {
×
171
                            deletions.insert(Cow::Borrowed(field.name), from);
×
172
                        }
×
173
                    }
174

175
                    for (field, to) in to_enum.fields() {
294✔
176
                        if !from_enum
7✔
177
                            .field_by_name(field.name)
7✔
178
                            .is_ok_and(|x| x.is_some())
7✔
179
                        {
×
180
                            insertions.insert(Cow::Borrowed(field.name), to);
×
181
                        }
7✔
182
                    }
183

184
                    Value::Struct {
294✔
185
                        updates,
294✔
186
                        deletions,
294✔
187
                        insertions,
294✔
188
                        unchanged,
294✔
189
                    }
294✔
190
                };
191

192
            // If there are no changes, return Equal instead of User
193
            let is_empty = match &value {
892✔
194
                Value::Tuple { updates } => updates.is_empty(),
598✔
195
                Value::Struct {
196
                    updates,
294✔
197
                    deletions,
294✔
198
                    insertions,
294✔
199
                    ..
200
                } => updates.is_empty() && deletions.is_empty() && insertions.is_empty(),
294✔
201
            };
202
            if is_empty {
892✔
203
                return Diff::Equal { value: Some(from) };
611✔
204
            }
281✔
205

206
            Diff::User {
281✔
207
                from: from_enum.shape(),
281✔
208
                to: to_enum.shape(),
281✔
209
                variant: Some(from_variant.name),
281✔
210
                value,
281✔
211
            }
281✔
212
        }
213
        ((Def::Option(_), _), (Def::Option(_), _)) => {
214
            let from_option = from.into_option().unwrap();
14✔
215
            let to_option = to.into_option().unwrap();
14✔
216

217
            let (Some(from_value), Some(to_value)) = (from_option.value(), to_option.value())
14✔
218
            else {
219
                return Diff::Replace { from, to };
3✔
220
            };
221

222
            // Use sequences::diff to properly handle nested diffs
223
            let updates = sequences::diff(vec![from_value], vec![to_value]);
11✔
224

225
            if updates.is_empty() {
11✔
226
                return Diff::Equal { value: Some(from) };
×
227
            }
11✔
228

229
            Diff::User {
11✔
230
                from: from.shape(),
11✔
231
                to: to.shape(),
11✔
232
                variant: Some("Some"),
11✔
233
                value: Value::Tuple { updates },
11✔
234
            }
11✔
235
        }
236
        (
237
            (Def::List(_) | Def::Slice(_), _) | (_, Type::Sequence(_)),
238
            (Def::List(_) | Def::Slice(_), _) | (_, Type::Sequence(_)),
239
        ) => {
240
            let from_list = from.into_list_like().unwrap();
188,758✔
241
            let to_list = to.into_list_like().unwrap();
188,758✔
242

243
            let updates = sequences::diff(
188,758✔
244
                from_list.iter().collect::<Vec<_>>(),
188,758✔
245
                to_list.iter().collect::<Vec<_>>(),
188,758✔
246
            );
247

248
            if updates.is_empty() {
188,758✔
249
                return Diff::Equal { value: Some(from) };
188,606✔
250
            }
152✔
251

252
            Diff::Sequence {
152✔
253
                from: from.shape(),
152✔
254
                to: to.shape(),
152✔
255
                updates,
152✔
256
            }
152✔
257
        }
258
        ((Def::DynamicValue(_), _), (Def::DynamicValue(_), _)) => diff_dynamic_values(from, to),
138✔
259
        // DynamicValue vs concrete type
260
        ((Def::DynamicValue(_), _), _) => diff_dynamic_vs_concrete(from, to, false),
211✔
261
        (_, (Def::DynamicValue(_), _)) => diff_dynamic_vs_concrete(to, from, true),
63✔
262
        _ => Diff::Replace { from, to },
1,925✔
263
    }
264
}
574,061✔
265

266
/// Diff two dynamic values (like `facet_value::Value`)
267
fn diff_dynamic_values<'mem, 'facet>(
138✔
268
    from: Peek<'mem, 'facet>,
138✔
269
    to: Peek<'mem, 'facet>,
138✔
270
) -> Diff<'mem, 'facet> {
138✔
271
    let from_dyn = from.into_dynamic_value().unwrap();
138✔
272
    let to_dyn = to.into_dynamic_value().unwrap();
138✔
273

274
    let from_kind = from_dyn.kind();
138✔
275
    let to_kind = to_dyn.kind();
138✔
276

277
    // If kinds differ, just return Replace
278
    if from_kind != to_kind {
138✔
279
        return Diff::Replace { from, to };
3✔
280
    }
135✔
281

282
    match from_kind {
135✔
283
        DynValueKind::Null => Diff::Equal { value: Some(from) },
×
284
        DynValueKind::Bool => {
285
            if from_dyn.as_bool() == to_dyn.as_bool() {
6✔
286
                Diff::Equal { value: Some(from) }
×
287
            } else {
288
                Diff::Replace { from, to }
6✔
289
            }
290
        }
291
        DynValueKind::Number => {
292
            // Compare numbers - try exact integer comparison first, then float
293
            let same = match (from_dyn.as_i64(), to_dyn.as_i64()) {
67✔
294
                (Some(l), Some(r)) => l == r,
67✔
295
                _ => match (from_dyn.as_u64(), to_dyn.as_u64()) {
×
296
                    (Some(l), Some(r)) => l == r,
×
297
                    _ => match (from_dyn.as_f64(), to_dyn.as_f64()) {
×
298
                        (Some(l), Some(r)) => l == r,
×
299
                        _ => false,
×
300
                    },
301
                },
302
            };
303
            if same {
67✔
304
                Diff::Equal { value: Some(from) }
×
305
            } else {
306
                Diff::Replace { from, to }
67✔
307
            }
308
        }
309
        DynValueKind::String => {
310
            if from_dyn.as_str() == to_dyn.as_str() {
21✔
311
                Diff::Equal { value: Some(from) }
×
312
            } else {
313
                Diff::Replace { from, to }
21✔
314
            }
315
        }
316
        DynValueKind::Bytes => {
317
            if from_dyn.as_bytes() == to_dyn.as_bytes() {
×
318
                Diff::Equal { value: Some(from) }
×
319
            } else {
320
                Diff::Replace { from, to }
×
321
            }
322
        }
323
        DynValueKind::Array => {
324
            // Use the sequence diff algorithm for arrays
325
            let from_iter = from_dyn.array_iter();
6✔
326
            let to_iter = to_dyn.array_iter();
6✔
327

328
            let from_elems: Vec<_> = from_iter.map(|i| i.collect()).unwrap_or_default();
6✔
329
            let to_elems: Vec<_> = to_iter.map(|i| i.collect()).unwrap_or_default();
6✔
330

331
            let updates = sequences::diff(from_elems, to_elems);
6✔
332

333
            if updates.is_empty() {
6✔
334
                return Diff::Equal { value: Some(from) };
×
335
            }
6✔
336

337
            Diff::Sequence {
6✔
338
                from: from.shape(),
6✔
339
                to: to.shape(),
6✔
340
                updates,
6✔
341
            }
6✔
342
        }
343
        DynValueKind::Object => {
344
            // Treat objects like struct diffs
345
            let from_len = from_dyn.object_len().unwrap_or(0);
35✔
346
            let to_len = to_dyn.object_len().unwrap_or(0);
35✔
347

348
            let mut updates = HashMap::new();
35✔
349
            let mut deletions = HashMap::new();
35✔
350
            let mut insertions = HashMap::new();
35✔
351
            let mut unchanged = HashSet::new();
35✔
352

353
            // Collect keys from `from`
354
            let mut from_keys: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
35✔
355
            for i in 0..from_len {
74✔
356
                if let Some((key, value)) = from_dyn.object_get_entry(i) {
74✔
357
                    from_keys.insert(key.to_owned(), value);
74✔
358
                }
74✔
359
            }
360

361
            // Collect keys from `to`
362
            let mut to_keys: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
35✔
363
            for i in 0..to_len {
74✔
364
                if let Some((key, value)) = to_dyn.object_get_entry(i) {
74✔
365
                    to_keys.insert(key.to_owned(), value);
74✔
366
                }
74✔
367
            }
368

369
            // Compare entries
370
            for (key, from_value) in &from_keys {
74✔
371
                if let Some(to_value) = to_keys.get(key) {
74✔
372
                    let diff = diff_new_peek(*from_value, *to_value);
72✔
373
                    if diff.is_equal() {
72✔
374
                        unchanged.insert(Cow::Owned(key.clone()));
24✔
375
                    } else {
48✔
376
                        updates.insert(Cow::Owned(key.clone()), diff);
48✔
377
                    }
48✔
378
                } else {
2✔
379
                    deletions.insert(Cow::Owned(key.clone()), *from_value);
2✔
380
                }
2✔
381
            }
382

383
            for (key, to_value) in &to_keys {
74✔
384
                if !from_keys.contains_key(key) {
74✔
385
                    insertions.insert(Cow::Owned(key.clone()), *to_value);
2✔
386
                }
72✔
387
            }
388

389
            let is_empty = updates.is_empty() && deletions.is_empty() && insertions.is_empty();
35✔
390
            if is_empty {
35✔
391
                return Diff::Equal { value: Some(from) };
×
392
            }
35✔
393

394
            Diff::User {
35✔
395
                from: from.shape(),
35✔
396
                to: to.shape(),
35✔
397
                variant: None,
35✔
398
                value: Value::Struct {
35✔
399
                    updates,
35✔
400
                    deletions,
35✔
401
                    insertions,
35✔
402
                    unchanged,
35✔
403
                },
35✔
404
            }
35✔
405
        }
406
        DynValueKind::DateTime => {
407
            // Compare datetime by their components
408
            if from_dyn.as_datetime() == to_dyn.as_datetime() {
×
409
                Diff::Equal { value: Some(from) }
×
410
            } else {
411
                Diff::Replace { from, to }
×
412
            }
413
        }
414
        DynValueKind::QName | DynValueKind::Uuid => {
415
            // For QName and Uuid, compare by their raw representation
416
            // Since they have the same kind, we can only compare by Replace semantics
417
            Diff::Replace { from, to }
×
418
        }
419
    }
420
}
138✔
421

422
/// Diff a DynamicValue against a concrete type
423
/// `dyn_peek` is the DynamicValue, `concrete_peek` is the concrete type
424
/// `swapped` indicates if the original from/to were swapped (true means dyn_peek is actually "to")
425
fn diff_dynamic_vs_concrete<'mem, 'facet>(
274✔
426
    dyn_peek: Peek<'mem, 'facet>,
274✔
427
    concrete_peek: Peek<'mem, 'facet>,
274✔
428
    swapped: bool,
274✔
429
) -> Diff<'mem, 'facet> {
274✔
430
    // Determine actual from/to based on swapped flag
431
    let (from_peek, to_peek) = if swapped {
274✔
432
        (concrete_peek, dyn_peek)
63✔
433
    } else {
434
        (dyn_peek, concrete_peek)
211✔
435
    };
436
    let dyn_val = dyn_peek.into_dynamic_value().unwrap();
274✔
437
    let dyn_kind = dyn_val.kind();
274✔
438

439
    // Try to match based on the DynamicValue's kind
440
    match dyn_kind {
274✔
441
        DynValueKind::Bool => {
442
            if concrete_peek
5✔
443
                .get::<bool>()
5✔
444
                .ok()
5✔
445
                .is_some_and(|&v| dyn_val.as_bool() == Some(v))
5✔
446
            {
447
                return Diff::Equal {
3✔
448
                    value: Some(from_peek),
3✔
449
                };
3✔
450
            }
2✔
451
        }
452
        DynValueKind::Number => {
453
            let is_equal =
207✔
454
                // Try signed integers
455
                concrete_peek.get::<i8>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
207✔
456
                || concrete_peek.get::<i16>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
207✔
457
                || concrete_peek.get::<i32>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
207✔
458
                || concrete_peek.get::<i64>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v))
205✔
459
                || concrete_peek.get::<isize>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
130✔
460
                // Try unsigned integers
461
                || concrete_peek.get::<u8>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
130✔
462
                || concrete_peek.get::<u16>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
125✔
463
                || concrete_peek.get::<u32>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
125✔
464
                || concrete_peek.get::<u64>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v))
125✔
465
                || concrete_peek.get::<usize>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
125✔
466
                // Try floats
467
                || concrete_peek.get::<f32>().ok().is_some_and(|&v| dyn_val.as_f64() == Some(v as f64))
125✔
468
                || concrete_peek.get::<f64>().ok().is_some_and(|&v| dyn_val.as_f64() == Some(v));
125✔
469
            if is_equal {
207✔
470
                return Diff::Equal {
83✔
471
                    value: Some(from_peek),
83✔
472
                };
83✔
473
            }
124✔
474
        }
475
        DynValueKind::String => {
476
            if concrete_peek
24✔
477
                .as_str()
24✔
478
                .is_some_and(|s| dyn_val.as_str() == Some(s))
24✔
479
            {
480
                return Diff::Equal {
14✔
481
                    value: Some(from_peek),
14✔
482
                };
14✔
483
            }
10✔
484
        }
485
        DynValueKind::Array => {
486
            // Try to diff as sequences if the concrete type is list-like
487
            if let Ok(concrete_list) = concrete_peek.into_list_like() {
21✔
488
                let dyn_elems: Vec<_> = dyn_val
19✔
489
                    .array_iter()
19✔
490
                    .map(|i| i.collect())
19✔
491
                    .unwrap_or_default();
19✔
492
                let concrete_elems: Vec<_> = concrete_list.iter().collect();
19✔
493

494
                // Use correct order based on swapped flag
495
                let (from_elems, to_elems) = if swapped {
19✔
496
                    (concrete_elems, dyn_elems)
5✔
497
                } else {
498
                    (dyn_elems, concrete_elems)
14✔
499
                };
500
                let updates = sequences::diff(from_elems, to_elems);
19✔
501

502
                if updates.is_empty() {
19✔
503
                    return Diff::Equal {
10✔
504
                        value: Some(from_peek),
10✔
505
                    };
10✔
506
                }
9✔
507

508
                return Diff::Sequence {
9✔
509
                    from: from_peek.shape(),
9✔
510
                    to: to_peek.shape(),
9✔
511
                    updates,
9✔
512
                };
9✔
513
            }
2✔
514
        }
515
        DynValueKind::Object => {
516
            // Try to diff as struct if the concrete type is a struct
517
            if let Ok(concrete_struct) = concrete_peek.into_struct() {
16✔
518
                let dyn_len = dyn_val.object_len().unwrap_or(0);
16✔
519

520
                let mut updates = HashMap::new();
16✔
521
                let mut deletions = HashMap::new();
16✔
522
                let mut insertions = HashMap::new();
16✔
523
                let mut unchanged = HashSet::new();
16✔
524

525
                // Collect keys from dynamic object
526
                let mut dyn_keys: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
16✔
527
                for i in 0..dyn_len {
34✔
528
                    if let Some((key, value)) = dyn_val.object_get_entry(i) {
34✔
529
                        dyn_keys.insert(key.to_owned(), value);
34✔
530
                    }
34✔
531
                }
532

533
                // Compare with concrete struct fields
534
                // When swapped, dyn is "to" and concrete is "from", so we need to swap the diff direction
535
                for (key, dyn_value) in &dyn_keys {
34✔
536
                    if let Ok(concrete_value) = concrete_struct.field_by_name(key) {
34✔
537
                        let diff = if swapped {
34✔
538
                            diff_new_peek(concrete_value, *dyn_value)
2✔
539
                        } else {
540
                            diff_new_peek(*dyn_value, concrete_value)
32✔
541
                        };
542
                        if diff.is_equal() {
34✔
543
                            unchanged.insert(Cow::Owned(key.clone()));
17✔
544
                        } else {
17✔
545
                            updates.insert(Cow::Owned(key.clone()), diff);
17✔
546
                        }
17✔
547
                    } else {
548
                        // Field in dyn but not in concrete
549
                        // If swapped: dyn is "to", so this is an insertion
550
                        // If not swapped: dyn is "from", so this is a deletion
551
                        if swapped {
×
552
                            insertions.insert(Cow::Owned(key.clone()), *dyn_value);
×
553
                        } else {
×
554
                            deletions.insert(Cow::Owned(key.clone()), *dyn_value);
×
555
                        }
×
556
                    }
557
                }
558

559
                for (field, concrete_value) in concrete_struct.fields() {
34✔
560
                    if !dyn_keys.contains_key(field.name) {
34✔
561
                        // Field in concrete but not in dyn
562
                        // If swapped: concrete is "from", so this is a deletion
563
                        // If not swapped: concrete is "to", so this is an insertion
564
                        if swapped {
×
565
                            deletions.insert(Cow::Borrowed(field.name), concrete_value);
×
566
                        } else {
×
567
                            insertions.insert(Cow::Borrowed(field.name), concrete_value);
×
568
                        }
×
569
                    }
34✔
570
                }
571

572
                let is_empty = updates.is_empty() && deletions.is_empty() && insertions.is_empty();
16✔
573
                if is_empty {
16✔
574
                    return Diff::Equal {
6✔
575
                        value: Some(from_peek),
6✔
576
                    };
6✔
577
                }
10✔
578

579
                return Diff::User {
10✔
580
                    from: from_peek.shape(),
10✔
581
                    to: to_peek.shape(),
10✔
582
                    variant: None,
10✔
583
                    value: Value::Struct {
10✔
584
                        updates,
10✔
585
                        deletions,
10✔
586
                        insertions,
10✔
587
                        unchanged,
10✔
588
                    },
10✔
589
                };
10✔
590
            }
×
591
        }
592
        // For other kinds (Null, Bytes, DateTime), fall through to Replace
593
        _ => {}
1✔
594
    }
595

596
    Diff::Replace {
139✔
597
        from: from_peek,
139✔
598
        to: to_peek,
139✔
599
    }
139✔
600
}
274✔
601

602
/// Dereference a pointer/reference to get the underlying value
603
fn deref_if_pointer<'mem, 'facet>(peek: Peek<'mem, 'facet>) -> Peek<'mem, 'facet> {
1,148,404✔
604
    if let Ok(ptr) = peek.into_pointer()
1,148,404✔
605
        && let Some(target) = ptr.borrow_inner()
282✔
606
    {
607
        return deref_if_pointer(target);
282✔
608
    }
1,148,122✔
609
    peek
1,148,122✔
610
}
1,148,404✔
611

612
pub(crate) fn diff_closeness(diff: &Diff<'_, '_>) -> usize {
×
613
    match diff {
×
614
        Diff::Equal { .. } => 1, // This does not actually matter for flattening sequence diffs, because all diffs there are non-equal
×
615
        Diff::Replace { .. } => 0,
×
616
        Diff::Sequence { updates, .. } => updates.closeness(),
×
617
        Diff::User {
618
            from, to, value, ..
×
619
        } => value.closeness() + (from == to) as usize,
×
620
    }
621
}
×
622

623
/// Collect all leaf-level changes with their paths.
624
///
625
/// This walks the diff tree recursively and collects every terminal change
626
/// (scalar replacements) along with the path to reach them. This is useful
627
/// for compact display: if there's only one leaf change deep in a tree,
628
/// you can show `path.to.field: old → new` instead of nested structure.
629
pub fn collect_leaf_changes<'mem, 'facet>(
×
630
    diff: &Diff<'mem, 'facet>,
×
631
) -> Vec<LeafChange<'mem, 'facet>> {
×
632
    let mut changes = Vec::new();
×
633
    collect_leaf_changes_inner(diff, Path::new(), &mut changes);
×
634
    changes
×
635
}
×
636

637
fn collect_leaf_changes_inner<'mem, 'facet>(
×
638
    diff: &Diff<'mem, 'facet>,
×
639
    path: Path,
×
640
    changes: &mut Vec<LeafChange<'mem, 'facet>>,
×
641
) {
×
642
    match diff {
×
643
        Diff::Equal { .. } => {
×
644
            // No change
×
645
        }
×
646
        Diff::Replace { from, to } => {
×
647
            // This is a leaf change
×
648
            changes.push(LeafChange {
×
649
                path,
×
650
                kind: LeafChangeKind::Replace {
×
651
                    from: *from,
×
652
                    to: *to,
×
653
                },
×
654
            });
×
655
        }
×
656
        Diff::User {
657
            value,
×
658
            variant,
×
659
            from,
×
660
            ..
661
        } => {
662
            // For Option::Some, skip the variant in the path since it's implied
663
            // (the value exists, so it's Some)
664
            let is_option = matches!(from.def, Def::Option(_));
×
665

666
            let base_path = if let Some(v) = variant {
×
667
                if is_option && *v == "Some" {
×
668
                    path // Skip "::Some" for options
×
669
                } else {
670
                    path.with(PathSegment::Variant(Cow::Borrowed(*v)))
×
671
                }
672
            } else {
673
                path
×
674
            };
675

676
            match value {
×
677
                Value::Struct {
678
                    updates,
×
679
                    deletions,
×
680
                    insertions,
×
681
                    ..
682
                } => {
683
                    // Recurse into field updates
684
                    for (field, diff) in updates {
×
685
                        let field_path = base_path.with(PathSegment::Field(field.clone()));
×
686
                        collect_leaf_changes_inner(diff, field_path, changes);
×
687
                    }
×
688
                    // Deletions are leaf changes
689
                    for (field, peek) in deletions {
×
690
                        let field_path = base_path.with(PathSegment::Field(field.clone()));
×
691
                        changes.push(LeafChange {
×
692
                            path: field_path,
×
693
                            kind: LeafChangeKind::Delete { value: *peek },
×
694
                        });
×
695
                    }
×
696
                    // Insertions are leaf changes
697
                    for (field, peek) in insertions {
×
698
                        let field_path = base_path.with(PathSegment::Field(field.clone()));
×
699
                        changes.push(LeafChange {
×
700
                            path: field_path,
×
701
                            kind: LeafChangeKind::Insert { value: *peek },
×
702
                        });
×
703
                    }
×
704
                }
705
                Value::Tuple { updates } => {
×
706
                    // For single-element tuples (like Option::Some), skip the index
707
                    if is_option {
×
708
                        // Recurse directly without adding [0]
×
709
                        collect_from_updates_for_single_elem(&base_path, updates, changes);
×
710
                    } else {
×
711
                        collect_from_updates(&base_path, updates, changes);
×
712
                    }
×
713
                }
714
            }
715
        }
716
        Diff::Sequence { updates, .. } => {
×
717
            collect_from_updates(&path, updates, changes);
×
718
        }
×
719
    }
720
}
×
721

722
/// Special handling for single-element tuples (like Option::Some)
723
/// where we want to skip the `[0]` index in the path.
724
fn collect_from_updates_for_single_elem<'mem, 'facet>(
×
725
    base_path: &Path,
×
726
    updates: &Updates<'mem, 'facet>,
×
727
    changes: &mut Vec<LeafChange<'mem, 'facet>>,
×
728
) {
×
729
    // For single-element tuples, we expect exactly one change
730
    // Just use base_path directly instead of adding [0]
731
    if let Some(update_group) = &updates.0.first {
×
732
        // Process the first replace group if present
733
        if let Some(replace) = &update_group.0.first
×
734
            && replace.removals.len() == 1
×
735
            && replace.additions.len() == 1
×
736
        {
737
            let from = replace.removals[0];
×
738
            let to = replace.additions[0];
×
739
            let nested = diff_new_peek(from, to);
×
740
            if matches!(nested, Diff::Replace { .. }) {
×
741
                changes.push(LeafChange {
×
742
                    path: base_path.clone(),
×
743
                    kind: LeafChangeKind::Replace { from, to },
×
744
                });
×
745
            } else {
×
746
                collect_leaf_changes_inner(&nested, base_path.clone(), changes);
×
747
            }
×
748
            return;
×
749
        }
×
750
        // Handle nested diffs
751
        if let Some(diffs) = &update_group.0.last {
×
752
            for diff in diffs {
×
753
                collect_leaf_changes_inner(diff, base_path.clone(), changes);
×
754
            }
×
755
            return;
×
756
        }
×
757
    }
×
758
    // Fallback: use regular handling
759
    collect_from_updates(base_path, updates, changes);
×
760
}
×
761

762
fn collect_from_updates<'mem, 'facet>(
×
763
    base_path: &Path,
×
764
    updates: &Updates<'mem, 'facet>,
×
765
    changes: &mut Vec<LeafChange<'mem, 'facet>>,
×
766
) {
×
767
    // Walk through the interspersed structure to collect changes with correct indices
768
    let mut index = 0;
×
769

770
    // Process first update group if present
771
    if let Some(update_group) = &updates.0.first {
×
772
        collect_from_update_group(base_path, update_group, &mut index, changes);
×
773
    }
×
774

775
    // Process interleaved (unchanged, update) pairs
776
    for (unchanged, update_group) in &updates.0.values {
×
777
        index += unchanged.len();
×
778
        collect_from_update_group(base_path, update_group, &mut index, changes);
×
779
    }
×
780

781
    // Trailing unchanged items don't add changes
782
}
×
783

784
fn collect_from_update_group<'mem, 'facet>(
×
785
    base_path: &Path,
×
786
    group: &crate::UpdatesGroup<'mem, 'facet>,
×
787
    index: &mut usize,
×
788
    changes: &mut Vec<LeafChange<'mem, 'facet>>,
×
789
) {
×
790
    // Process first replace group if present
791
    if let Some(replace) = &group.0.first {
×
792
        collect_from_replace_group(base_path, replace, index, changes);
×
793
    }
×
794

795
    // Process interleaved (diffs, replace) pairs
796
    for (diffs, replace) in &group.0.values {
×
797
        for diff in diffs {
×
798
            let elem_path = base_path.with(PathSegment::Index(*index));
×
799
            collect_leaf_changes_inner(diff, elem_path, changes);
×
800
            *index += 1;
×
801
        }
×
802
        collect_from_replace_group(base_path, replace, index, changes);
×
803
    }
804

805
    // Process trailing diffs
806
    if let Some(diffs) = &group.0.last {
×
807
        for diff in diffs {
×
808
            let elem_path = base_path.with(PathSegment::Index(*index));
×
809
            collect_leaf_changes_inner(diff, elem_path, changes);
×
810
            *index += 1;
×
811
        }
×
812
    }
×
813
}
×
814

815
fn collect_from_replace_group<'mem, 'facet>(
×
816
    base_path: &Path,
×
817
    group: &crate::ReplaceGroup<'mem, 'facet>,
×
818
    index: &mut usize,
×
819
    changes: &mut Vec<LeafChange<'mem, 'facet>>,
×
820
) {
×
821
    // For replace groups, we have removals and additions
822
    // If counts match, treat as 1:1 replacements at the same index
823
    // Otherwise, show as deletions followed by insertions
824

825
    if group.removals.len() == group.additions.len() {
×
826
        // 1:1 replacements
827
        for (from, to) in group.removals.iter().zip(group.additions.iter()) {
×
828
            let elem_path = base_path.with(PathSegment::Index(*index));
×
829
            // Check if this is actually a nested diff
830
            let nested = diff_new_peek(*from, *to);
×
831
            if matches!(nested, Diff::Replace { .. }) {
×
832
                changes.push(LeafChange {
×
833
                    path: elem_path,
×
834
                    kind: LeafChangeKind::Replace {
×
835
                        from: *from,
×
836
                        to: *to,
×
837
                    },
×
838
                });
×
839
            } else {
×
840
                collect_leaf_changes_inner(&nested, elem_path, changes);
×
841
            }
×
842
            *index += 1;
×
843
        }
844
    } else {
845
        // Mixed deletions and insertions
846
        for from in &group.removals {
×
847
            let elem_path = base_path.with(PathSegment::Index(*index));
×
848
            changes.push(LeafChange {
×
849
                path: elem_path.clone(),
×
850
                kind: LeafChangeKind::Delete { value: *from },
×
851
            });
×
852
            *index += 1;
×
853
        }
×
854
        // Insertions happen at current index
855
        for to in &group.additions {
×
856
            let elem_path = base_path.with(PathSegment::Index(*index));
×
857
            changes.push(LeafChange {
×
858
                path: elem_path,
×
859
                kind: LeafChangeKind::Insert { value: *to },
×
860
            });
×
861
            *index += 1;
×
862
        }
×
863
    }
864
}
×
865

866
/// A single leaf-level change in a diff, with path information.
867
#[derive(Debug, Clone)]
868
pub struct LeafChange<'mem, 'facet> {
869
    /// The path from root to this change
870
    pub path: Path,
871
    /// The kind of change
872
    pub kind: LeafChangeKind<'mem, 'facet>,
873
}
874

875
/// The kind of leaf change.
876
#[derive(Debug, Clone)]
877
pub enum LeafChangeKind<'mem, 'facet> {
878
    /// A value was replaced
879
    Replace {
880
        /// The old value
881
        from: Peek<'mem, 'facet>,
882
        /// The new value
883
        to: Peek<'mem, 'facet>,
884
    },
885
    /// A value was deleted
886
    Delete {
887
        /// The deleted value
888
        value: Peek<'mem, 'facet>,
889
    },
890
    /// A value was inserted
891
    Insert {
892
        /// The inserted value
893
        value: Peek<'mem, 'facet>,
894
    },
895
}
896

897
impl<'mem, 'facet> LeafChange<'mem, 'facet> {
898
    /// Format this change without colors.
899
    pub fn format_plain(&self) -> String {
×
900
        use facet_pretty::PrettyPrinter;
901

902
        let printer = PrettyPrinter::default()
×
903
            .with_colors(false)
×
904
            .with_minimal_option_names(true);
×
905

906
        let mut out = String::new();
×
907

908
        // Show path if non-empty
909
        if !self.path.0.is_empty() {
×
910
            out.push_str(&format!("{}: ", self.path));
×
911
        }
×
912

913
        match &self.kind {
×
914
            LeafChangeKind::Replace { from, to } => {
×
915
                out.push_str(&format!(
×
916
                    "{} → {}",
×
917
                    printer.format_peek(*from),
×
918
                    printer.format_peek(*to)
×
919
                ));
×
920
            }
×
921
            LeafChangeKind::Delete { value } => {
×
922
                out.push_str(&format!("- {}", printer.format_peek(*value)));
×
923
            }
×
924
            LeafChangeKind::Insert { value } => {
×
925
                out.push_str(&format!("+ {}", printer.format_peek(*value)));
×
926
            }
×
927
        }
928

929
        out
×
930
    }
×
931

932
    /// Format this change with colors.
933
    pub fn format_colored(&self) -> String {
×
934
        use facet_pretty::{PrettyPrinter, tokyo_night};
935
        use owo_colors::OwoColorize;
936

937
        let printer = PrettyPrinter::default()
×
938
            .with_colors(false)
×
939
            .with_minimal_option_names(true);
×
940

941
        let mut out = String::new();
×
942

943
        // Show path if non-empty (in field name color)
944
        if !self.path.0.is_empty() {
×
945
            out.push_str(&format!(
×
946
                "{}: ",
×
947
                format!("{}", self.path).color(tokyo_night::FIELD_NAME)
×
948
            ));
×
949
        }
×
950

951
        match &self.kind {
×
952
            LeafChangeKind::Replace { from, to } => {
×
953
                out.push_str(&format!(
×
954
                    "{} {} {}",
×
955
                    printer.format_peek(*from).color(tokyo_night::DELETION),
×
956
                    "→".color(tokyo_night::COMMENT),
×
957
                    printer.format_peek(*to).color(tokyo_night::INSERTION)
×
958
                ));
×
959
            }
×
960
            LeafChangeKind::Delete { value } => {
×
961
                out.push_str(&format!(
×
962
                    "{} {}",
×
963
                    "-".color(tokyo_night::DELETION),
×
964
                    printer.format_peek(*value).color(tokyo_night::DELETION)
×
965
                ));
×
966
            }
×
967
            LeafChangeKind::Insert { value } => {
×
968
                out.push_str(&format!(
×
969
                    "{} {}",
×
970
                    "+".color(tokyo_night::INSERTION),
×
971
                    printer.format_peek(*value).color(tokyo_night::INSERTION)
×
972
                ));
×
973
            }
×
974
        }
975

976
        out
×
977
    }
×
978
}
979

980
impl<'mem, 'facet> std::fmt::Display for LeafChange<'mem, 'facet> {
981
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
982
        write!(f, "{}", self.format_plain())
×
983
    }
×
984
}
985

986
/// Configuration for diff formatting.
987
#[derive(Debug, Clone)]
988
pub struct DiffFormat {
989
    /// Use colors in output
990
    pub colors: bool,
991
    /// Maximum number of changes before switching to summary mode
992
    pub max_inline_changes: usize,
993
    /// Whether to use compact (path-based) format for few changes
994
    pub prefer_compact: bool,
995
}
996

997
impl Default for DiffFormat {
998
    fn default() -> Self {
×
999
        Self {
×
1000
            colors: true,
×
1001
            max_inline_changes: 10,
×
1002
            prefer_compact: true,
×
1003
        }
×
1004
    }
×
1005
}
1006

1007
/// Format the diff with the given configuration.
1008
///
1009
/// This chooses between compact (path-based) and tree (nested) format
1010
/// based on the number of changes and the configuration.
1011
pub fn format_diff(diff: &Diff<'_, '_>, config: &DiffFormat) -> String {
×
1012
    if matches!(diff, Diff::Equal { .. }) {
×
1013
        return if config.colors {
×
1014
            use facet_pretty::tokyo_night;
1015
            use owo_colors::OwoColorize;
1016
            "(no changes)".color(tokyo_night::MUTED).to_string()
×
1017
        } else {
1018
            "(no changes)".to_string()
×
1019
        };
1020
    }
×
1021

1022
    let changes = collect_leaf_changes(diff);
×
1023

1024
    if changes.is_empty() {
×
1025
        return if config.colors {
×
1026
            use facet_pretty::tokyo_night;
1027
            use owo_colors::OwoColorize;
1028
            "(no changes)".color(tokyo_night::MUTED).to_string()
×
1029
        } else {
1030
            "(no changes)".to_string()
×
1031
        };
1032
    }
×
1033

1034
    // Use compact format if preferred and we have few changes
1035
    if config.prefer_compact && changes.len() <= config.max_inline_changes {
×
1036
        let mut out = String::new();
×
1037
        for (i, change) in changes.iter().enumerate() {
×
1038
            if i > 0 {
×
1039
                out.push('\n');
×
1040
            }
×
1041
            if config.colors {
×
1042
                out.push_str(&change.format_colored());
×
1043
            } else {
×
1044
                out.push_str(&change.format_plain());
×
1045
            }
×
1046
        }
1047
        return out;
×
1048
    }
×
1049

1050
    // Fall back to tree format for many changes
1051
    if changes.len() > config.max_inline_changes {
×
1052
        let mut out = String::new();
×
1053

1054
        // Show first few changes
1055
        for (i, change) in changes.iter().take(config.max_inline_changes).enumerate() {
×
1056
            if i > 0 {
×
1057
                out.push('\n');
×
1058
            }
×
1059
            if config.colors {
×
1060
                out.push_str(&change.format_colored());
×
1061
            } else {
×
1062
                out.push_str(&change.format_plain());
×
1063
            }
×
1064
        }
1065

1066
        // Show summary of remaining
1067
        let remaining = changes.len() - config.max_inline_changes;
×
1068
        if remaining > 0 {
×
1069
            out.push('\n');
×
1070
            let summary = format!(
×
1071
                "... and {} more change{}",
1072
                remaining,
1073
                if remaining == 1 { "" } else { "s" }
×
1074
            );
1075
            if config.colors {
×
1076
                use facet_pretty::tokyo_night;
1077
                use owo_colors::OwoColorize;
1078
                out.push_str(&summary.color(tokyo_night::MUTED).to_string());
×
1079
            } else {
×
1080
                out.push_str(&summary);
×
1081
            }
×
1082
        }
×
1083
        return out;
×
1084
    }
×
1085

1086
    // Default: use Display impl (tree format)
1087
    format!("{diff}")
×
1088
}
×
1089

1090
/// Format the diff with default configuration.
1091
pub fn format_diff_default(diff: &Diff<'_, '_>) -> String {
×
1092
    format_diff(diff, &DiffFormat::default())
×
1093
}
×
1094

1095
/// Format the diff in compact mode (path-based, no tree structure).
1096
pub fn format_diff_compact(diff: &Diff<'_, '_>) -> String {
×
1097
    format_diff(
×
1098
        diff,
×
1099
        &DiffFormat {
×
1100
            prefer_compact: true,
×
1101
            max_inline_changes: usize::MAX,
×
1102
            ..Default::default()
×
1103
        },
×
1104
    )
1105
}
×
1106

1107
/// Format the diff in compact mode without colors.
1108
pub fn format_diff_compact_plain(diff: &Diff<'_, '_>) -> String {
×
1109
    format_diff(
×
1110
        diff,
×
1111
        &DiffFormat {
×
1112
            colors: false,
×
1113
            prefer_compact: true,
×
1114
            max_inline_changes: usize::MAX,
×
1115
        },
×
1116
    )
1117
}
×
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