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

facet-rs / facet / 19775426569

28 Nov 2025 11:06PM UTC coverage: 60.423% (+0.8%) from 59.624%
19775426569

push

github

fasterthanlime
Add facet-assert/diff/pretty support for facet_value::Value

This adds full support for comparing, diffing, and pretty-printing
`facet_value::Value` types through the facet reflection system.

Key changes:

- facet-reflect: Add `PeekDynamicValue` for inspecting DynamicValue types
  - Methods for accessing scalars (bool, number, string, bytes)
  - Iterators for arrays and objects
  - `Peek::into_dynamic_value()` method

- facet-assert: Enable structural comparison between Value and concrete types
  - `value!([1, 2, 3])` can now be compared with `vec![1, 2, 3]`
  - Value strings compare equal to String
  - Value numbers compare equal to numeric types
  - Handles nested structures recursively

- facet-diff: Add DynamicValue diffing support
  - Arrays produce Diff::Sequence with element diffs
  - Objects produce Diff::User with field-level diffs

- facet-pretty: Add DynamicValue formatting
  - Handles all DynValueKind variants
  - Proper indentation for nested arrays/objects

- Tests: 27 tests covering all functionality

356 of 588 new or added lines in 5 files covered. (60.54%)

2 existing lines in 2 files now uncovered.

17185 of 28441 relevant lines covered (60.42%)

152.92 hits per line

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

39.75
/facet-diff/src/diff.rs
1
use std::collections::{HashMap, HashSet};
2

3
use facet::{Def, DynValueKind, Shape, StructKind, Type, UserType};
4
use facet_core::Facet;
5
use facet_reflect::{HasFields, Peek};
6

7
use crate::sequences::{self, Updates};
8

9
/// The difference between two values.
10
///
11
/// The `from` value does not necessarily have to have the same type as the `to` value.
12
pub enum Diff<'mem, 'facet> {
13
    /// The two values are equal
14
    Equal,
15

16
    /// Fallback case.
17
    ///
18
    /// We do not know much about the values, apart from that they are unequal to each other.
19
    Replace {
20
        /// The `from` value.
21
        from: Peek<'mem, 'facet>,
22

23
        /// The `to` value.
24
        to: Peek<'mem, 'facet>,
25
    },
26

27
    /// The two values are both structures or both enums with similar variants.
28
    User {
29
        /// The shape of the `from` struct.
30
        from: &'static Shape,
31

32
        /// The shape of the `to` struct.
33
        to: &'static Shape,
34

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

38
        /// The value of the struct/enum variant (tuple or struct fields)
39
        value: Value<'mem, 'facet>,
40
    },
41

42
    /// A diff between two sequences
43
    Sequence {
44
        /// The shape of the `from` sequence.
45
        from: &'static Shape,
46

47
        /// The shape of the `to` sequence.
48
        to: &'static Shape,
49

50
        /// The updates on the sequence
51
        updates: Updates<'mem, 'facet>,
52
    },
53
}
54

55
/// A set of updates, additions, deletions, insertions etc. for a tuple or a struct
56
pub enum Value<'mem, 'facet> {
57
    Tuple {
58
        /// The updates on the sequence
59
        updates: Updates<'mem, 'facet>,
60
    },
61

62
    Struct {
63
        /// The fields that are updated between the structs
64
        updates: HashMap<&'static str, Diff<'mem, 'facet>>,
65

66
        /// The fields that are in `from` but not in `to`.
67
        deletions: HashMap<&'static str, Peek<'mem, 'facet>>,
68

69
        /// The fields that are in `to` but not in `from`.
70
        insertions: HashMap<&'static str, Peek<'mem, 'facet>>,
71

72
        /// The fields that are unchanged
73
        unchanged: HashSet<&'static str>,
74
    },
75
}
76

77
impl<'mem, 'facet> Value<'mem, 'facet> {
78
    fn closeness(&self) -> usize {
×
79
        match self {
×
80
            Self::Tuple { updates } => updates.closeness(),
×
81
            Self::Struct { unchanged, .. } => unchanged.len(),
×
82
        }
83
    }
×
84
}
85

86
/// Extension trait that provides a [`diff`](FacetDiff::diff) method for `Facet` types
87
pub trait FacetDiff<'f>: Facet<'f> {
88
    /// Computes the difference between two values that implement `Facet`
89
    fn diff<'a, U: Facet<'f>>(&'a self, other: &'a U) -> Diff<'a, 'f>;
90
}
91

92
impl<'f, T: Facet<'f>> FacetDiff<'f> for T {
93
    fn diff<'a, U: Facet<'f>>(&'a self, other: &'a U) -> Diff<'a, 'f> {
5✔
94
        Diff::new(self, other)
5✔
95
    }
5✔
96
}
97

98
impl<'mem, 'facet> Diff<'mem, 'facet> {
99
    /// Returns true if the two values were equal
100
    pub fn is_equal(&self) -> bool {
21✔
101
        matches!(self, Self::Equal)
21✔
102
    }
21✔
103

104
    /// Computes the difference between two values that implement `Facet`
105
    pub fn new<T: Facet<'facet>, U: Facet<'facet>>(from: &'mem T, to: &'mem U) -> Self {
5✔
106
        Self::new_peek(Peek::new(from), Peek::new(to))
5✔
107
    }
5✔
108

109
    pub(crate) fn new_peek(from: Peek<'mem, 'facet>, to: Peek<'mem, 'facet>) -> Self {
22✔
110
        if from.shape().id == to.shape().id && from.shape().is_partial_eq() && from == to {
22✔
111
            return Diff::Equal;
8✔
112
        }
14✔
113

114
        match (
115
            (from.shape().def, from.shape().ty),
14✔
116
            (to.shape().def, to.shape().ty),
14✔
117
        ) {
118
            (
119
                (_, Type::User(UserType::Struct(from_ty))),
×
120
                (_, Type::User(UserType::Struct(to_ty))),
×
121
            ) if from_ty.kind == to_ty.kind => {
×
122
                let from_ty = from.into_struct().unwrap();
×
123
                let to_ty = to.into_struct().unwrap();
×
124

125
                let value =
×
126
                    if [StructKind::Tuple, StructKind::TupleStruct].contains(&from_ty.ty().kind) {
×
127
                        let from = from_ty.fields().map(|x| x.1).collect();
×
128
                        let to = to_ty.fields().map(|x| x.1).collect();
×
129

130
                        let updates = sequences::diff(from, to);
×
131

132
                        Value::Tuple { updates }
×
133
                    } else {
134
                        let mut updates = HashMap::new();
×
135
                        let mut deletions = HashMap::new();
×
136
                        let mut insertions = HashMap::new();
×
137
                        let mut unchanged = HashSet::new();
×
138

139
                        for (field, from) in from_ty.fields() {
×
140
                            if let Ok(to) = to_ty.field_by_name(field.name) {
×
141
                                let diff = Diff::new_peek(from, to);
×
142
                                if diff.is_equal() {
×
143
                                    unchanged.insert(field.name);
×
144
                                } else {
×
145
                                    updates.insert(field.name, diff);
×
146
                                }
×
147
                            } else {
×
148
                                deletions.insert(field.name, from);
×
149
                            }
×
150
                        }
151

152
                        for (field, to) in to_ty.fields() {
×
153
                            if from_ty.field_by_name(field.name).is_err() {
×
154
                                insertions.insert(field.name, to);
×
155
                            }
×
156
                        }
157
                        Value::Struct {
×
158
                            updates,
×
159
                            deletions,
×
160
                            insertions,
×
161
                            unchanged,
×
162
                        }
×
163
                    };
164

165
                Diff::User {
×
166
                    from: from.shape(),
×
167
                    to: to.shape(),
×
168
                    variant: None,
×
169
                    value,
×
170
                }
×
171
            }
172
            ((_, Type::User(UserType::Enum(_))), (_, Type::User(UserType::Enum(_)))) => {
173
                let from_enum = from.into_enum().unwrap();
×
174
                let to_enum = to.into_enum().unwrap();
×
175

176
                let from_variant = from_enum.active_variant().unwrap();
×
177
                let to_variant = to_enum.active_variant().unwrap();
×
178

179
                if from_variant.name != to_variant.name
×
180
                    || from_variant.data.kind != to_variant.data.kind
×
181
                {
182
                    return Diff::Replace { from, to };
×
183
                }
×
184

185
                let value = if [StructKind::Tuple, StructKind::TupleStruct]
×
186
                    .contains(&from_variant.data.kind)
×
187
                {
188
                    let from = from_enum.fields().map(|x| x.1).collect();
×
189
                    let to = to_enum.fields().map(|x| x.1).collect();
×
190

191
                    let updates = sequences::diff(from, to);
×
192

193
                    Value::Tuple { updates }
×
194
                } else {
195
                    let mut updates = HashMap::new();
×
196
                    let mut deletions = HashMap::new();
×
197
                    let mut insertions = HashMap::new();
×
198
                    let mut unchanged = HashSet::new();
×
199

200
                    for (field, from) in from_enum.fields() {
×
201
                        if let Ok(Some(to)) = to_enum.field_by_name(field.name) {
×
202
                            let diff = Diff::new_peek(from, to);
×
203
                            if diff.is_equal() {
×
204
                                unchanged.insert(field.name);
×
205
                            } else {
×
206
                                updates.insert(field.name, diff);
×
207
                            }
×
208
                        } else {
×
209
                            deletions.insert(field.name, from);
×
210
                        }
×
211
                    }
212

213
                    for (field, to) in to_enum.fields() {
×
214
                        if !from_enum
×
215
                            .field_by_name(field.name)
×
216
                            .is_ok_and(|x| x.is_some())
×
217
                        {
×
218
                            insertions.insert(field.name, to);
×
219
                        }
×
220
                    }
221

222
                    Value::Struct {
×
223
                        updates,
×
224
                        deletions,
×
225
                        insertions,
×
226
                        unchanged,
×
227
                    }
×
228
                };
229

230
                Diff::User {
×
231
                    from: from_enum.shape(),
×
232
                    to: to_enum.shape(),
×
233
                    variant: Some(from_variant.name),
×
234
                    value,
×
235
                }
×
236
            }
237
            ((Def::Option(_), _), (Def::Option(_), _)) => {
238
                let from_option = from.into_option().unwrap();
×
239
                let to_option = to.into_option().unwrap();
×
240

241
                let (Some(from_value), Some(to_value)) = (from_option.value(), to_option.value())
×
242
                else {
243
                    return Diff::Replace { from, to };
×
244
                };
245

246
                let mut updates = Updates::default();
×
247

248
                let diff = Self::new_peek(from_value, to_value);
×
249
                if !diff.is_equal() {
×
250
                    updates.push_add(to_value);
×
251
                    updates.push_remove(from_value);
×
252
                }
×
253

254
                Diff::User {
×
255
                    from: from.shape(),
×
256
                    to: to.shape(),
×
257
                    variant: Some("Some"),
×
258
                    value: Value::Tuple { updates },
×
259
                }
×
260
            }
261
            (
262
                (Def::List(_), _) | (_, Type::Sequence(_)),
263
                (Def::List(_), _) | (_, Type::Sequence(_)),
264
            ) => {
265
                let from_list = from.into_list_like().unwrap();
×
266
                let to_list = to.into_list_like().unwrap();
×
267

268
                let updates = sequences::diff(
×
269
                    from_list.iter().collect::<Vec<_>>(),
×
270
                    to_list.iter().collect::<Vec<_>>(),
×
271
                );
272

273
                Diff::Sequence {
×
274
                    from: from.shape(),
×
275
                    to: to.shape(),
×
276
                    updates,
×
277
                }
×
278
            }
279
            ((Def::DynamicValue(_), _), (Def::DynamicValue(_), _)) => {
280
                Self::diff_dynamic_values(from, to)
14✔
281
            }
UNCOV
282
            _ => Diff::Replace { from, to },
×
283
        }
284
    }
22✔
285

286
    /// Diff two dynamic values (like `facet_value::Value`)
287
    fn diff_dynamic_values(from: Peek<'mem, 'facet>, to: Peek<'mem, 'facet>) -> Self {
14✔
288
        let from_dyn = from.into_dynamic_value().unwrap();
14✔
289
        let to_dyn = to.into_dynamic_value().unwrap();
14✔
290

291
        let from_kind = from_dyn.kind();
14✔
292
        let to_kind = to_dyn.kind();
14✔
293

294
        // If kinds differ, just return Replace
295
        if from_kind != to_kind {
14✔
NEW
296
            return Diff::Replace { from, to };
×
297
        }
14✔
298

299
        match from_kind {
14✔
NEW
300
            DynValueKind::Null => Diff::Equal,
×
301
            DynValueKind::Bool => {
NEW
302
                if from_dyn.as_bool() == to_dyn.as_bool() {
×
NEW
303
                    Diff::Equal
×
304
                } else {
NEW
305
                    Diff::Replace { from, to }
×
306
                }
307
            }
308
            DynValueKind::Number => {
309
                // Compare numbers - try exact integer comparison first, then float
310
                let same = match (from_dyn.as_i64(), to_dyn.as_i64()) {
11✔
311
                    (Some(l), Some(r)) => l == r,
11✔
NEW
312
                    _ => match (from_dyn.as_u64(), to_dyn.as_u64()) {
×
NEW
313
                        (Some(l), Some(r)) => l == r,
×
NEW
314
                        _ => match (from_dyn.as_f64(), to_dyn.as_f64()) {
×
NEW
315
                            (Some(l), Some(r)) => l == r,
×
NEW
316
                            _ => false,
×
317
                        },
318
                    },
319
                };
320
                if same {
11✔
NEW
321
                    Diff::Equal
×
322
                } else {
323
                    Diff::Replace { from, to }
11✔
324
                }
325
            }
326
            DynValueKind::String => {
NEW
327
                if from_dyn.as_str() == to_dyn.as_str() {
×
NEW
328
                    Diff::Equal
×
329
                } else {
NEW
330
                    Diff::Replace { from, to }
×
331
                }
332
            }
333
            DynValueKind::Bytes => {
NEW
334
                if from_dyn.as_bytes() == to_dyn.as_bytes() {
×
NEW
335
                    Diff::Equal
×
336
                } else {
NEW
337
                    Diff::Replace { from, to }
×
338
                }
339
            }
340
            DynValueKind::Array => {
341
                // Use the sequence diff algorithm for arrays
342
                let from_iter = from_dyn.array_iter();
1✔
343
                let to_iter = to_dyn.array_iter();
1✔
344

345
                let from_elems: Vec<_> = from_iter.map(|i| i.collect()).unwrap_or_default();
1✔
346
                let to_elems: Vec<_> = to_iter.map(|i| i.collect()).unwrap_or_default();
1✔
347

348
                let updates = sequences::diff(from_elems, to_elems);
1✔
349

350
                Diff::Sequence {
1✔
351
                    from: from.shape(),
1✔
352
                    to: to.shape(),
1✔
353
                    updates,
1✔
354
                }
1✔
355
            }
356
            DynValueKind::Object => {
357
                // Treat objects like struct diffs
358
                let from_len = from_dyn.object_len().unwrap_or(0);
2✔
359
                let to_len = to_dyn.object_len().unwrap_or(0);
2✔
360

361
                let mut updates = HashMap::new();
2✔
362
                let mut deletions = HashMap::new();
2✔
363
                let mut insertions = HashMap::new();
2✔
364
                let mut unchanged = HashSet::new();
2✔
365

366
                // Collect keys from `from`
367
                let mut from_keys: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
2✔
368
                for i in 0..from_len {
4✔
369
                    if let Some((key, value)) = from_dyn.object_get_entry(i) {
4✔
370
                        from_keys.insert(key.to_owned(), value);
4✔
371
                    }
4✔
372
                }
373

374
                // Collect keys from `to`
375
                let mut to_keys: HashMap<String, Peek<'mem, 'facet>> = HashMap::new();
2✔
376
                for i in 0..to_len {
4✔
377
                    if let Some((key, value)) = to_dyn.object_get_entry(i) {
4✔
378
                        to_keys.insert(key.to_owned(), value);
4✔
379
                    }
4✔
380
                }
381

382
                // Compare entries
383
                for (key, from_value) in &from_keys {
6✔
384
                    if let Some(to_value) = to_keys.get(key) {
4✔
385
                        let diff = Self::new_peek(*from_value, *to_value);
3✔
386
                        if diff.is_equal() {
3✔
387
                            unchanged.insert(key.as_str());
2✔
388
                        } else {
2✔
389
                            // We need to leak the key to get a static lifetime
1✔
390
                            // This is a limitation of the current API
1✔
391
                            let key_static: &'static str = Box::leak(key.clone().into_boxed_str());
1✔
392
                            updates.insert(key_static, diff);
1✔
393
                        }
1✔
394
                    } else {
1✔
395
                        let key_static: &'static str = Box::leak(key.clone().into_boxed_str());
1✔
396
                        deletions.insert(key_static, *from_value);
1✔
397
                    }
1✔
398
                }
399

400
                for (key, to_value) in &to_keys {
6✔
401
                    if !from_keys.contains_key(key) {
4✔
402
                        let key_static: &'static str = Box::leak(key.clone().into_boxed_str());
1✔
403
                        insertions.insert(key_static, *to_value);
1✔
404
                    }
3✔
405
                }
406

407
                // Convert unchanged HashSet<&str> to HashSet<&'static str>
408
                let unchanged_static: HashSet<&'static str> = unchanged
2✔
409
                    .into_iter()
2✔
410
                    .map(|s| -> &'static str { Box::leak(s.to_owned().into_boxed_str()) })
2✔
411
                    .collect();
2✔
412

413
                Diff::User {
2✔
414
                    from: from.shape(),
2✔
415
                    to: to.shape(),
2✔
416
                    variant: None,
2✔
417
                    value: Value::Struct {
2✔
418
                        updates,
2✔
419
                        deletions,
2✔
420
                        insertions,
2✔
421
                        unchanged: unchanged_static,
2✔
422
                    },
2✔
423
                }
2✔
424
            }
425
        }
426
    }
14✔
427

428
    pub(crate) fn closeness(&self) -> usize {
1✔
429
        match self {
1✔
430
            Self::Equal => 1, // This does not actually matter for flattening sequence diffs, because all diffs there are non-equal
×
431
            Self::Replace { .. } => 0,
1✔
432
            Self::Sequence { updates, .. } => updates.closeness(),
×
433
            Self::User {
×
434
                from, to, value, ..
×
435
            } => value.closeness() + (from == to) as usize,
×
436
        }
437
    }
1✔
438
}
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