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

facet-rs / facet / 19803020611

30 Nov 2025 06:23PM UTC coverage: 57.923% (-3.0%) from 60.927%
19803020611

push

github

fasterthanlime
Convert facet-kdl to use define_attr_grammar!

- Replace ~140 lines of hand-written macros with concise grammar DSL
- Enhance make_parse_attr.rs grammar compiler:
  - Add `ns "kdl";` syntax for namespace declaration
  - Add to_snake_case() helper (NodeName → node_name)
  - Generate __attr! macro with proper ExtensionAttr return
  - Use () data for unit variants, full dispatch for complex ones
  - Add #[repr(u8)] to generated enums for Facet derive
  - Use HashMap for O(1) struct lookup
- Update design diagrams with namespace fixes

50 of 53 new or added lines in 1 file covered. (94.34%)

3583 existing lines in 40 files now uncovered.

18788 of 32436 relevant lines covered (57.92%)

179.8 hits per line

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

90.63
/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, Shape, StructKind, Type, UserType};
8
use facet_core::Facet;
9
use facet_reflect::{HasFields, Peek};
10

11
use crate::sequences::{self, Updates};
12

13
/// The difference between two values.
14
///
15
/// The `from` value does not necessarily have to have the same type as the `to` value.
16
pub enum Diff<'mem, 'facet> {
17
    /// The two values are equal
18
    Equal {
19
        /// The value (stored for display purposes)
20
        value: Option<Peek<'mem, 'facet>>,
21
    },
22

23
    /// Fallback case.
24
    ///
25
    /// We do not know much about the values, apart from that they are unequal to each other.
26
    Replace {
27
        /// The `from` value.
28
        from: Peek<'mem, 'facet>,
29

30
        /// The `to` value.
31
        to: Peek<'mem, 'facet>,
32
    },
33

34
    /// The two values are both structures or both enums with similar variants.
35
    User {
36
        /// The shape of the `from` struct.
37
        from: &'static Shape,
38

39
        /// The shape of the `to` struct.
40
        to: &'static Shape,
41

42
        /// The name of the variant, this is [`None`] if the values are structs
43
        variant: Option<&'static str>,
44

45
        /// The value of the struct/enum variant (tuple or struct fields)
46
        value: Value<'mem, 'facet>,
47
    },
48

49
    /// A diff between two sequences
50
    Sequence {
51
        /// The shape of the `from` sequence.
52
        from: &'static Shape,
53

54
        /// The shape of the `to` sequence.
55
        to: &'static Shape,
56

57
        /// The updates on the sequence
58
        updates: Updates<'mem, 'facet>,
59
    },
60
}
61

62
/// A set of updates, additions, deletions, insertions etc. for a tuple or a struct
63
pub enum Value<'mem, 'facet> {
64
    Tuple {
65
        /// The updates on the sequence
66
        updates: Updates<'mem, 'facet>,
67
    },
68

69
    Struct {
70
        /// The fields that are updated between the structs
71
        updates: HashMap<Cow<'static, str>, Diff<'mem, 'facet>>,
72

73
        /// The fields that are in `from` but not in `to`.
74
        deletions: HashMap<Cow<'static, str>, Peek<'mem, 'facet>>,
75

76
        /// The fields that are in `to` but not in `from`.
77
        insertions: HashMap<Cow<'static, str>, Peek<'mem, 'facet>>,
78

79
        /// The fields that are unchanged
80
        unchanged: HashSet<Cow<'static, str>>,
81
    },
82
}
83

84
impl<'mem, 'facet> Value<'mem, 'facet> {
85
    fn closeness(&self) -> usize {
45✔
86
        match self {
45✔
87
            Self::Tuple { updates } => updates.closeness(),
13✔
88
            Self::Struct { unchanged, .. } => unchanged.len(),
32✔
89
        }
90
    }
45✔
91
}
92

93
/// Extension trait that provides a [`diff`](FacetDiff::diff) method for `Facet` types
94
pub trait FacetDiff<'f>: Facet<'f> {
95
    /// Computes the difference between two values that implement `Facet`
96
    fn diff<'a, U: Facet<'f>>(&'a self, other: &'a U) -> Diff<'a, 'f>;
97
}
98

99
impl<'f, T: Facet<'f>> FacetDiff<'f> for T {
100
    fn diff<'a, U: Facet<'f>>(&'a self, other: &'a U) -> Diff<'a, 'f> {
131✔
101
        Diff::new(self, other)
131✔
102
    }
131✔
103
}
104

105
impl<'mem, 'facet> Diff<'mem, 'facet> {
106
    /// Returns true if the two values were equal
107
    pub fn is_equal(&self) -> bool {
1,692✔
108
        matches!(self, Self::Equal { .. })
1,692✔
109
    }
1,692✔
110

111
    /// Computes the difference between two values that implement `Facet`
112
    pub fn new<T: Facet<'facet>, U: Facet<'facet>>(from: &'mem T, to: &'mem U) -> Self {
131✔
113
        Self::new_peek(Peek::new(from), Peek::new(to))
131✔
114
    }
131✔
115

116
    pub(crate) fn new_peek(from: Peek<'mem, 'facet>, to: Peek<'mem, 'facet>) -> Self {
2,139✔
117
        // Dereference pointers/references to compare the underlying values
118
        let from = Self::deref_if_pointer(from);
2,139✔
119
        let to = Self::deref_if_pointer(to);
2,139✔
120

121
        if from.shape().id == to.shape().id && from.shape().is_partial_eq() && from == to {
2,139✔
122
            return Diff::Equal { value: Some(from) };
334✔
123
        }
1,805✔
124

125
        match (
126
            (from.shape().def, from.shape().ty),
1,805✔
127
            (to.shape().def, to.shape().ty),
1,805✔
128
        ) {
129
            (
130
                (_, Type::User(UserType::Struct(from_ty))),
92✔
131
                (_, Type::User(UserType::Struct(to_ty))),
92✔
132
            ) if from_ty.kind == to_ty.kind => {
92✔
133
                let from_ty = from.into_struct().unwrap();
92✔
134
                let to_ty = to.into_struct().unwrap();
92✔
135

136
                let value =
92✔
137
                    if [StructKind::Tuple, StructKind::TupleStruct].contains(&from_ty.ty().kind) {
92✔
138
                        let from = from_ty.fields().map(|x| x.1).collect();
29✔
139
                        let to = to_ty.fields().map(|x| x.1).collect();
29✔
140

141
                        let updates = sequences::diff(from, to);
29✔
142

143
                        Value::Tuple { updates }
29✔
144
                    } else {
145
                        let mut updates = HashMap::new();
63✔
146
                        let mut deletions = HashMap::new();
63✔
147
                        let mut insertions = HashMap::new();
63✔
148
                        let mut unchanged = HashSet::new();
63✔
149

150
                        for (field, from) in from_ty.fields() {
183✔
151
                            if let Ok(to) = to_ty.field_by_name(field.name) {
183✔
152
                                let diff = Diff::new_peek(from, to);
182✔
153
                                if diff.is_equal() {
182✔
154
                                    unchanged.insert(Cow::Borrowed(field.name));
73✔
155
                                } else {
109✔
156
                                    updates.insert(Cow::Borrowed(field.name), diff);
109✔
157
                                }
109✔
158
                            } else {
1✔
159
                                deletions.insert(Cow::Borrowed(field.name), from);
1✔
160
                            }
1✔
161
                        }
162

163
                        for (field, to) in to_ty.fields() {
183✔
164
                            if from_ty.field_by_name(field.name).is_err() {
183✔
165
                                insertions.insert(Cow::Borrowed(field.name), to);
1✔
166
                            }
182✔
167
                        }
168
                        Value::Struct {
63✔
169
                            updates,
63✔
170
                            deletions,
63✔
171
                            insertions,
63✔
172
                            unchanged,
63✔
173
                        }
63✔
174
                    };
175

176
                Diff::User {
92✔
177
                    from: from.shape(),
92✔
178
                    to: to.shape(),
92✔
179
                    variant: None,
92✔
180
                    value,
92✔
181
                }
92✔
182
            }
183
            ((_, Type::User(UserType::Enum(_))), (_, Type::User(UserType::Enum(_)))) => {
184
                let from_enum = from.into_enum().unwrap();
16✔
185
                let to_enum = to.into_enum().unwrap();
16✔
186

187
                let from_variant = from_enum.active_variant().unwrap();
16✔
188
                let to_variant = to_enum.active_variant().unwrap();
16✔
189

190
                if from_variant.name != to_variant.name
16✔
191
                    || from_variant.data.kind != to_variant.data.kind
12✔
192
                {
193
                    return Diff::Replace { from, to };
4✔
194
                }
12✔
195

196
                let value = if [StructKind::Tuple, StructKind::TupleStruct]
12✔
197
                    .contains(&from_variant.data.kind)
12✔
198
                {
199
                    let from = from_enum.fields().map(|x| x.1).collect();
9✔
200
                    let to = to_enum.fields().map(|x| x.1).collect();
9✔
201

202
                    let updates = sequences::diff(from, to);
9✔
203

204
                    Value::Tuple { updates }
9✔
205
                } else {
206
                    let mut updates = HashMap::new();
3✔
207
                    let mut deletions = HashMap::new();
3✔
208
                    let mut insertions = HashMap::new();
3✔
209
                    let mut unchanged = HashSet::new();
3✔
210

211
                    for (field, from) in from_enum.fields() {
3✔
212
                        if let Ok(Some(to)) = to_enum.field_by_name(field.name) {
2✔
213
                            let diff = Diff::new_peek(from, to);
2✔
214
                            if diff.is_equal() {
2✔
215
                                unchanged.insert(Cow::Borrowed(field.name));
1✔
216
                            } else {
1✔
217
                                updates.insert(Cow::Borrowed(field.name), diff);
1✔
218
                            }
1✔
219
                        } else {
×
UNCOV
220
                            deletions.insert(Cow::Borrowed(field.name), from);
×
UNCOV
221
                        }
×
222
                    }
223

224
                    for (field, to) in to_enum.fields() {
3✔
225
                        if !from_enum
2✔
226
                            .field_by_name(field.name)
2✔
227
                            .is_ok_and(|x| x.is_some())
2✔
UNCOV
228
                        {
×
UNCOV
229
                            insertions.insert(Cow::Borrowed(field.name), to);
×
230
                        }
2✔
231
                    }
232

233
                    Value::Struct {
3✔
234
                        updates,
3✔
235
                        deletions,
3✔
236
                        insertions,
3✔
237
                        unchanged,
3✔
238
                    }
3✔
239
                };
240

241
                Diff::User {
12✔
242
                    from: from_enum.shape(),
12✔
243
                    to: to_enum.shape(),
12✔
244
                    variant: Some(from_variant.name),
12✔
245
                    value,
12✔
246
                }
12✔
247
            }
248
            ((Def::Option(_), _), (Def::Option(_), _)) => {
249
                let from_option = from.into_option().unwrap();
21✔
250
                let to_option = to.into_option().unwrap();
21✔
251

252
                let (Some(from_value), Some(to_value)) = (from_option.value(), to_option.value())
21✔
253
                else {
254
                    return Diff::Replace { from, to };
3✔
255
                };
256

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

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

274
                let updates = sequences::diff(
19✔
275
                    from_list.iter().collect::<Vec<_>>(),
19✔
276
                    to_list.iter().collect::<Vec<_>>(),
19✔
277
                );
278

279
                Diff::Sequence {
19✔
280
                    from: from.shape(),
19✔
281
                    to: to.shape(),
19✔
282
                    updates,
19✔
283
                }
19✔
284
            }
285
            ((Def::DynamicValue(_), _), (Def::DynamicValue(_), _)) => {
286
                Self::diff_dynamic_values(from, to)
165✔
287
            }
288
            // DynamicValue vs concrete type
289
            ((Def::DynamicValue(_), _), _) => Self::diff_dynamic_vs_concrete(from, to, false),
219✔
290
            (_, (Def::DynamicValue(_), _)) => Self::diff_dynamic_vs_concrete(to, from, true),
77✔
291
            _ => Diff::Replace { from, to },
1,196✔
292
        }
293
    }
2,139✔
294

295
    /// Diff two dynamic values (like `facet_value::Value`)
296
    fn diff_dynamic_values(from: Peek<'mem, 'facet>, to: Peek<'mem, 'facet>) -> Self {
165✔
297
        let from_dyn = from.into_dynamic_value().unwrap();
165✔
298
        let to_dyn = to.into_dynamic_value().unwrap();
165✔
299

300
        let from_kind = from_dyn.kind();
165✔
301
        let to_kind = to_dyn.kind();
165✔
302

303
        // If kinds differ, just return Replace
304
        if from_kind != to_kind {
165✔
305
            return Diff::Replace { from, to };
3✔
306
        }
162✔
307

308
        match from_kind {
162✔
UNCOV
309
            DynValueKind::Null => Diff::Equal { value: Some(from) },
×
310
            DynValueKind::Bool => {
311
                if from_dyn.as_bool() == to_dyn.as_bool() {
13✔
312
                    Diff::Equal { value: Some(from) }
×
313
                } else {
314
                    Diff::Replace { from, to }
13✔
315
                }
316
            }
317
            DynValueKind::Number => {
318
                // Compare numbers - try exact integer comparison first, then float
319
                let same = match (from_dyn.as_i64(), to_dyn.as_i64()) {
68✔
320
                    (Some(l), Some(r)) => l == r,
68✔
321
                    _ => match (from_dyn.as_u64(), to_dyn.as_u64()) {
×
UNCOV
322
                        (Some(l), Some(r)) => l == r,
×
UNCOV
323
                        _ => match (from_dyn.as_f64(), to_dyn.as_f64()) {
×
UNCOV
324
                            (Some(l), Some(r)) => l == r,
×
UNCOV
325
                            _ => false,
×
326
                        },
327
                    },
328
                };
329
                if same {
68✔
330
                    Diff::Equal { value: Some(from) }
×
331
                } else {
332
                    Diff::Replace { from, to }
68✔
333
                }
334
            }
335
            DynValueKind::String => {
336
                if from_dyn.as_str() == to_dyn.as_str() {
30✔
337
                    Diff::Equal { value: Some(from) }
×
338
                } else {
339
                    Diff::Replace { from, to }
30✔
340
                }
341
            }
342
            DynValueKind::Bytes => {
UNCOV
343
                if from_dyn.as_bytes() == to_dyn.as_bytes() {
×
UNCOV
344
                    Diff::Equal { value: Some(from) }
×
345
                } else {
UNCOV
346
                    Diff::Replace { from, to }
×
347
                }
348
            }
349
            DynValueKind::Array => {
350
                // Use the sequence diff algorithm for arrays
351
                let from_iter = from_dyn.array_iter();
5✔
352
                let to_iter = to_dyn.array_iter();
5✔
353

354
                let from_elems: Vec<_> = from_iter.map(|i| i.collect()).unwrap_or_default();
5✔
355
                let to_elems: Vec<_> = to_iter.map(|i| i.collect()).unwrap_or_default();
5✔
356

357
                let updates = sequences::diff(from_elems, to_elems);
5✔
358

359
                Diff::Sequence {
5✔
360
                    from: from.shape(),
5✔
361
                    to: to.shape(),
5✔
362
                    updates,
5✔
363
                }
5✔
364
            }
365
            DynValueKind::Object => {
366
                // Treat objects like struct diffs
367
                let from_len = from_dyn.object_len().unwrap_or(0);
46✔
368
                let to_len = to_dyn.object_len().unwrap_or(0);
46✔
369

370
                let mut updates = HashMap::new();
46✔
371
                let mut deletions = HashMap::new();
46✔
372
                let mut insertions = HashMap::new();
46✔
373
                let mut unchanged = HashSet::new();
46✔
374

375
                // Collect keys from `from`
376
                let mut from_keys: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
46✔
377
                for i in 0..from_len {
107✔
378
                    if let Some((key, value)) = from_dyn.object_get_entry(i) {
107✔
379
                        from_keys.insert(key.to_owned(), value);
107✔
380
                    }
107✔
381
                }
382

383
                // Collect keys from `to`
384
                let mut to_keys: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
46✔
385
                for i in 0..to_len {
107✔
386
                    if let Some((key, value)) = to_dyn.object_get_entry(i) {
107✔
387
                        to_keys.insert(key.to_owned(), value);
107✔
388
                    }
107✔
389
                }
390

391
                // Compare entries
392
                for (key, from_value) in &from_keys {
153✔
393
                    if let Some(to_value) = to_keys.get(key) {
107✔
394
                        let diff = Self::new_peek(*from_value, *to_value);
105✔
395
                        if diff.is_equal() {
105✔
396
                            unchanged.insert(Cow::Owned(key.clone()));
33✔
397
                        } else {
72✔
398
                            updates.insert(Cow::Owned(key.clone()), diff);
72✔
399
                        }
72✔
400
                    } else {
2✔
401
                        deletions.insert(Cow::Owned(key.clone()), *from_value);
2✔
402
                    }
2✔
403
                }
404

405
                for (key, to_value) in &to_keys {
153✔
406
                    if !from_keys.contains_key(key) {
107✔
407
                        insertions.insert(Cow::Owned(key.clone()), *to_value);
2✔
408
                    }
105✔
409
                }
410

411
                Diff::User {
46✔
412
                    from: from.shape(),
46✔
413
                    to: to.shape(),
46✔
414
                    variant: None,
46✔
415
                    value: Value::Struct {
46✔
416
                        updates,
46✔
417
                        deletions,
46✔
418
                        insertions,
46✔
419
                        unchanged,
46✔
420
                    },
46✔
421
                }
46✔
422
            }
423
            DynValueKind::DateTime => {
424
                // Compare datetime by their components
UNCOV
425
                if from_dyn.as_datetime() == to_dyn.as_datetime() {
×
UNCOV
426
                    Diff::Equal { value: Some(from) }
×
427
                } else {
UNCOV
428
                    Diff::Replace { from, to }
×
429
                }
430
            }
431
        }
432
    }
165✔
433

434
    /// Diff a DynamicValue against a concrete type
435
    /// `dyn_peek` is the DynamicValue, `concrete_peek` is the concrete type
436
    /// `swapped` indicates if the original from/to were swapped (true means dyn_peek is actually "to")
437
    fn diff_dynamic_vs_concrete(
296✔
438
        dyn_peek: Peek<'mem, 'facet>,
296✔
439
        concrete_peek: Peek<'mem, 'facet>,
296✔
440
        swapped: bool,
296✔
441
    ) -> Self {
296✔
442
        // Determine actual from/to based on swapped flag
443
        let (from_peek, to_peek) = if swapped {
296✔
444
            (concrete_peek, dyn_peek)
77✔
445
        } else {
446
            (dyn_peek, concrete_peek)
219✔
447
        };
448
        let dyn_val = dyn_peek.into_dynamic_value().unwrap();
296✔
449
        let dyn_kind = dyn_val.kind();
296✔
450

451
        // Try to match based on the DynamicValue's kind
452
        match dyn_kind {
296✔
453
            DynValueKind::Bool => {
454
                if concrete_peek
3✔
455
                    .get::<bool>()
3✔
456
                    .ok()
3✔
457
                    .is_some_and(|&v| dyn_val.as_bool() == Some(v))
3✔
458
                {
459
                    return Diff::Equal {
2✔
460
                        value: Some(from_peek),
2✔
461
                    };
2✔
462
                }
1✔
463
            }
464
            DynValueKind::Number => {
465
                let is_equal =
210✔
466
                    // Try signed integers
467
                    concrete_peek.get::<i8>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
210✔
468
                    || concrete_peek.get::<i16>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
210✔
469
                    || concrete_peek.get::<i32>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
210✔
470
                    || concrete_peek.get::<i64>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v))
208✔
471
                    || concrete_peek.get::<isize>().ok().is_some_and(|&v| dyn_val.as_i64() == Some(v as i64))
150✔
472
                    // Try unsigned integers
473
                    || concrete_peek.get::<u8>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
150✔
474
                    || concrete_peek.get::<u16>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
140✔
475
                    || concrete_peek.get::<u32>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
140✔
476
                    || concrete_peek.get::<u64>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v))
140✔
477
                    || concrete_peek.get::<usize>().ok().is_some_and(|&v| dyn_val.as_u64() == Some(v as u64))
140✔
478
                    // Try floats
479
                    || concrete_peek.get::<f32>().ok().is_some_and(|&v| dyn_val.as_f64() == Some(v as f64))
140✔
480
                    || concrete_peek.get::<f64>().ok().is_some_and(|&v| dyn_val.as_f64() == Some(v));
140✔
481
                if is_equal {
210✔
482
                    return Diff::Equal {
71✔
483
                        value: Some(from_peek),
71✔
484
                    };
71✔
485
                }
139✔
486
            }
487
            DynValueKind::String => {
488
                if concrete_peek
37✔
489
                    .as_str()
37✔
490
                    .is_some_and(|s| dyn_val.as_str() == Some(s))
37✔
491
                {
492
                    return Diff::Equal {
17✔
493
                        value: Some(from_peek),
17✔
494
                    };
17✔
495
                }
20✔
496
            }
497
            DynValueKind::Array => {
498
                // Try to diff as sequences if the concrete type is list-like
499
                if let Ok(concrete_list) = concrete_peek.into_list_like() {
14✔
500
                    let dyn_elems: Vec<_> = dyn_val
12✔
501
                        .array_iter()
12✔
502
                        .map(|i| i.collect())
12✔
503
                        .unwrap_or_default();
12✔
504
                    let concrete_elems: Vec<_> = concrete_list.iter().collect();
12✔
505

506
                    // Use correct order based on swapped flag
507
                    let (from_elems, to_elems) = if swapped {
12✔
508
                        (concrete_elems, dyn_elems)
4✔
509
                    } else {
510
                        (dyn_elems, concrete_elems)
8✔
511
                    };
512
                    let updates = sequences::diff(from_elems, to_elems);
12✔
513

514
                    return Diff::Sequence {
12✔
515
                        from: from_peek.shape(),
12✔
516
                        to: to_peek.shape(),
12✔
517
                        updates,
12✔
518
                    };
12✔
519
                }
2✔
520
            }
521
            DynValueKind::Object => {
522
                // Try to diff as struct if the concrete type is a struct
523
                if let Ok(concrete_struct) = concrete_peek.into_struct() {
31✔
524
                    let dyn_len = dyn_val.object_len().unwrap_or(0);
31✔
525

526
                    let mut updates = HashMap::new();
31✔
527
                    let mut deletions = HashMap::new();
31✔
528
                    let mut insertions = HashMap::new();
31✔
529
                    let mut unchanged = HashSet::new();
31✔
530

531
                    // Collect keys from dynamic object
532
                    let mut dyn_keys: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
31✔
533
                    for i in 0..dyn_len {
64✔
534
                        if let Some((key, value)) = dyn_val.object_get_entry(i) {
64✔
535
                            dyn_keys.insert(key.to_owned(), value);
64✔
536
                        }
64✔
537
                    }
538

539
                    // Compare with concrete struct fields
540
                    // When swapped, dyn is "to" and concrete is "from", so we need to swap the diff direction
541
                    for (key, dyn_value) in &dyn_keys {
95✔
542
                        if let Ok(concrete_value) = concrete_struct.field_by_name(key) {
64✔
543
                            let diff = if swapped {
64✔
544
                                Self::new_peek(concrete_value, *dyn_value)
2✔
545
                            } else {
546
                                Self::new_peek(*dyn_value, concrete_value)
62✔
547
                            };
548
                            if diff.is_equal() {
64✔
549
                                unchanged.insert(Cow::Owned(key.clone()));
25✔
550
                            } else {
39✔
551
                                updates.insert(Cow::Owned(key.clone()), diff);
39✔
552
                            }
39✔
553
                        } else {
554
                            // Field in dyn but not in concrete
555
                            // If swapped: dyn is "to", so this is an insertion
556
                            // If not swapped: dyn is "from", so this is a deletion
UNCOV
557
                            if swapped {
×
UNCOV
558
                                insertions.insert(Cow::Owned(key.clone()), *dyn_value);
×
UNCOV
559
                            } else {
×
UNCOV
560
                                deletions.insert(Cow::Owned(key.clone()), *dyn_value);
×
UNCOV
561
                            }
×
562
                        }
563
                    }
564

565
                    for (field, concrete_value) in concrete_struct.fields() {
64✔
566
                        if !dyn_keys.contains_key(field.name) {
64✔
567
                            // Field in concrete but not in dyn
568
                            // If swapped: concrete is "from", so this is a deletion
569
                            // If not swapped: concrete is "to", so this is an insertion
UNCOV
570
                            if swapped {
×
UNCOV
571
                                deletions.insert(Cow::Borrowed(field.name), concrete_value);
×
UNCOV
572
                            } else {
×
UNCOV
573
                                insertions.insert(Cow::Borrowed(field.name), concrete_value);
×
UNCOV
574
                            }
×
575
                        }
64✔
576
                    }
577

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

595
        Diff::Replace {
163✔
596
            from: from_peek,
163✔
597
            to: to_peek,
163✔
598
        }
163✔
599
    }
296✔
600

601
    /// Dereference a pointer/reference to get the underlying value
602
    fn deref_if_pointer(peek: Peek<'mem, 'facet>) -> Peek<'mem, 'facet> {
4,339✔
603
        if let Ok(ptr) = peek.into_pointer() {
4,339✔
604
            if let Some(target) = ptr.borrow_inner() {
61✔
605
                return Self::deref_if_pointer(target);
61✔
UNCOV
606
            }
×
607
        }
4,278✔
608
        peek
4,278✔
609
    }
4,339✔
610

611
    pub(crate) fn closeness(&self) -> usize {
256✔
612
        match self {
256✔
613
            Self::Equal { .. } => 1, // This does not actually matter for flattening sequence diffs, because all diffs there are non-equal
14✔
614
            Self::Replace { .. } => 0,
197✔
UNCOV
615
            Self::Sequence { updates, .. } => updates.closeness(),
×
616
            Self::User {
45✔
617
                from, to, value, ..
45✔
618
            } => value.closeness() + (from == to) as usize,
45✔
619
        }
620
    }
256✔
621
}
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