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

facet-rs / facet / 19902770885

03 Dec 2025 05:23PM UTC coverage: 59.013% (+0.1%) from 58.902%
19902770885

push

github

fasterthanlime
Add floating-point tolerance options to assert_same via builder pattern

Implements #1009: Adds a new `SameOptions` builder and `assert_same_with!`
macro to support floating-point tolerance in structural comparisons.

Key changes:
- Add `SameOptions` struct with `float_tolerance(f64)` builder method
- Add `check_same_with()` function that accepts options
- Add `assert_same_with!` and `debug_assert_same_with!` macros
- Update `Differ` to hold options and use tolerance when comparing floats
- Support tolerance for both f32 and f64 scalar types
- Support tolerance in DynamicValue number comparisons

Example usage:
```rust
use facet_assert::{assert_same_with, SameOptions};

let a = 1.0000001_f64;
let b = 1.0000002_f64;

// With tolerance:
assert_same_with!(a, b, SameOptions::new().float_tolerance(1e-6));
```

The builder pattern allows for future extensibility without breaking changes.

110 of 121 new or added lines in 2 files covered. (90.91%)

20694 of 35067 relevant lines covered (59.01%)

537.93 hits per line

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

53.13
/facet-assert/src/same.rs
1
//! Structural sameness checking for Facet types.
2

3
use core::fmt;
4
use facet_core::{Def, DynValueKind, Facet, Type, UserType};
5
use facet_pretty::PrettyPrinter;
6
use facet_reflect::{HasFields, Peek, ScalarType};
7

8
/// Options for customizing structural comparison behavior.
9
///
10
/// Use the builder pattern to configure options:
11
///
12
/// ```
13
/// use facet_assert::SameOptions;
14
///
15
/// let options = SameOptions::new()
16
///     .float_tolerance(1e-6);
17
/// ```
18
#[derive(Debug, Clone, Default)]
19
pub struct SameOptions {
20
    /// Tolerance for floating-point comparisons.
21
    /// If set, two floats are considered equal if their absolute difference
22
    /// is less than or equal to this value.
23
    float_tolerance: Option<f64>,
24
}
25

26
impl SameOptions {
27
    /// Create a new `SameOptions` with default settings (exact comparison).
28
    pub fn new() -> Self {
10✔
29
        Self::default()
10✔
30
    }
10✔
31

32
    /// Set the tolerance for floating-point comparisons.
33
    ///
34
    /// When set, two `f32` or `f64` values are considered equal if:
35
    /// `|left - right| <= tolerance`
36
    ///
37
    /// # Example
38
    ///
39
    /// ```
40
    /// use facet_assert::{assert_same_with, SameOptions};
41
    ///
42
    /// let a = 1.0000001_f64;
43
    /// let b = 1.0000002_f64;
44
    ///
45
    /// // This would fail with exact comparison:
46
    /// // assert_same!(a, b);
47
    ///
48
    /// // But passes with tolerance:
49
    /// assert_same_with!(a, b, SameOptions::new().float_tolerance(1e-6));
50
    /// ```
51
    pub fn float_tolerance(mut self, tolerance: f64) -> Self {
10✔
52
        self.float_tolerance = Some(tolerance);
10✔
53
        self
10✔
54
    }
10✔
55
}
56

57
/// Result of checking if two values are structurally the same.
58
pub enum Sameness {
59
    /// The values are structurally the same.
60
    Same,
61
    /// The values differ - contains a formatted diff.
62
    Different(String),
63
    /// Encountered an opaque type that cannot be compared.
64
    Opaque {
65
        /// The type name of the opaque type.
66
        type_name: &'static str,
67
    },
68
}
69

70
/// Check if two Facet values are structurally the same.
71
///
72
/// This does NOT require `PartialEq` - it walks the structure via reflection.
73
/// Two values are "same" if they have the same structure and values, even if
74
/// they have different type names.
75
///
76
/// Returns [`Sameness::Opaque`] if either value contains an opaque type.
77
pub fn check_same<'f, T: Facet<'f>, U: Facet<'f>>(left: &T, right: &U) -> Sameness {
29✔
78
    check_same_with(left, right, SameOptions::default())
29✔
79
}
29✔
80

81
/// Check if two Facet values are structurally the same, with custom options.
82
///
83
/// Like [`check_same`], but allows configuring comparison behavior via [`SameOptions`].
84
///
85
/// # Example
86
///
87
/// ```
88
/// use facet_assert::{check_same_with, SameOptions, Sameness};
89
///
90
/// let a = 1.0000001_f64;
91
/// let b = 1.0000002_f64;
92
///
93
/// // With tolerance, these are considered the same
94
/// let options = SameOptions::new().float_tolerance(1e-6);
95
/// assert!(matches!(check_same_with(&a, &b, options), Sameness::Same));
96
/// ```
97
pub fn check_same_with<'f, T: Facet<'f>, U: Facet<'f>>(
35✔
98
    left: &T,
35✔
99
    right: &U,
35✔
100
    options: SameOptions,
35✔
101
) -> Sameness {
35✔
102
    let left_peek = Peek::new(left);
35✔
103
    let right_peek = Peek::new(right);
35✔
104

105
    let mut differ = Differ::new(options);
35✔
106
    match differ.check(left_peek, right_peek) {
35✔
107
        CheckResult::Same => Sameness::Same,
22✔
108
        CheckResult::Different => Sameness::Different(differ.into_diff()),
13✔
109
        CheckResult::Opaque { type_name } => Sameness::Opaque { type_name },
×
110
    }
111
}
35✔
112

113
enum CheckResult {
114
    Same,
115
    Different,
116
    Opaque { type_name: &'static str },
117
}
118

119
struct Differ {
120
    /// Differences found, stored as lines
121
    diffs: Vec<DiffLine>,
122
    /// Current path for context
123
    path: Vec<PathSegment>,
124
    /// Comparison options
125
    options: SameOptions,
126
}
127

128
enum PathSegment {
129
    Field(&'static str),
130
    Index(usize),
131
    Variant(&'static str),
132
    Key(String),
133
}
134

135
impl fmt::Display for PathSegment {
136
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10✔
137
        match self {
10✔
138
            PathSegment::Field(name) => write!(f, ".{name}"),
2✔
139
            PathSegment::Index(i) => write!(f, "[{i}]"),
7✔
140
            PathSegment::Variant(name) => write!(f, "::{name}"),
×
141
            PathSegment::Key(k) => write!(f, "[{k:?}]"),
1✔
142
        }
143
    }
10✔
144
}
145

146
enum DiffLine {
147
    Changed {
148
        path: String,
149
        left: String,
150
        right: String,
151
    },
152
    OnlyLeft {
153
        path: String,
154
        value: String,
155
    },
156
    OnlyRight {
157
        path: String,
158
        value: String,
159
    },
160
}
161

162
impl Differ {
163
    fn new(options: SameOptions) -> Self {
39✔
164
        Self {
39✔
165
            diffs: Vec::new(),
39✔
166
            path: Vec::new(),
39✔
167
            options,
39✔
168
        }
39✔
169
    }
39✔
170

171
    /// Compare two f64 values, using tolerance if configured.
172
    fn floats_equal(&self, left: f64, right: f64) -> bool {
14✔
173
        if let Some(tolerance) = self.options.float_tolerance {
14✔
174
            (left - right).abs() <= tolerance
14✔
175
        } else {
NEW
176
            left == right
×
177
        }
178
    }
14✔
179

180
    /// Try to extract f64 values from two Peek values if they are both floats.
181
    /// Returns None if either value is not a float type.
182
    fn extract_floats(&self, left: Peek<'_, '_>, right: Peek<'_, '_>) -> Option<(f64, f64)> {
15✔
183
        let left_f64 = match left.scalar_type()? {
15✔
184
            ScalarType::F64 => *left.get::<f64>().ok()?,
13✔
185
            ScalarType::F32 => *left.get::<f32>().ok()? as f64,
1✔
186
            _ => return None,
1✔
187
        };
188
        let right_f64 = match right.scalar_type()? {
14✔
189
            ScalarType::F64 => *right.get::<f64>().ok()?,
13✔
190
            ScalarType::F32 => *right.get::<f32>().ok()? as f64,
1✔
NEW
191
            _ => return None,
×
192
        };
193
        Some((left_f64, right_f64))
14✔
194
    }
15✔
195

196
    fn current_path(&self) -> String {
15✔
197
        if self.path.is_empty() {
15✔
198
            "root".to_string()
5✔
199
        } else {
200
            let mut s = String::new();
10✔
201
            for seg in &self.path {
10✔
202
                s.push_str(&seg.to_string());
10✔
203
            }
10✔
204
            s
10✔
205
        }
206
    }
15✔
207

208
    fn format_value(peek: Peek<'_, '_>) -> String {
101✔
209
        let printer = PrettyPrinter::default().with_colors(false);
101✔
210
        printer.format_peek(peek).to_string()
101✔
211
    }
101✔
212

213
    fn record_changed(&mut self, left: Peek<'_, '_>, right: Peek<'_, '_>) {
14✔
214
        self.diffs.push(DiffLine::Changed {
14✔
215
            path: self.current_path(),
14✔
216
            left: Self::format_value(left),
14✔
217
            right: Self::format_value(right),
14✔
218
        });
14✔
219
    }
14✔
220

221
    fn record_only_left(&mut self, left: Peek<'_, '_>) {
1✔
222
        self.diffs.push(DiffLine::OnlyLeft {
1✔
223
            path: self.current_path(),
1✔
224
            value: Self::format_value(left),
1✔
225
        });
1✔
226
    }
1✔
227

228
    fn record_only_right(&mut self, right: Peek<'_, '_>) {
×
229
        self.diffs.push(DiffLine::OnlyRight {
×
230
            path: self.current_path(),
×
231
            value: Self::format_value(right),
×
232
        });
×
233
    }
×
234

235
    fn into_diff(self) -> String {
13✔
236
        use std::fmt::Write;
237

238
        let mut out = String::new();
13✔
239

240
        for diff in self.diffs {
15✔
241
            match diff {
15✔
242
                DiffLine::Changed { path, left, right } => {
14✔
243
                    writeln!(out, "\x1b[1m{path}\x1b[0m:").unwrap();
14✔
244
                    writeln!(out, "  \x1b[31m- {left}\x1b[0m").unwrap();
14✔
245
                    writeln!(out, "  \x1b[32m+ {right}\x1b[0m").unwrap();
14✔
246
                }
14✔
247
                DiffLine::OnlyLeft { path, value } => {
1✔
248
                    writeln!(out, "\x1b[1m{path}\x1b[0m (only in left):").unwrap();
1✔
249
                    writeln!(out, "  \x1b[31m- {value}\x1b[0m").unwrap();
1✔
250
                }
1✔
251
                DiffLine::OnlyRight { path, value } => {
×
252
                    writeln!(out, "\x1b[1m{path}\x1b[0m (only in right):").unwrap();
×
253
                    writeln!(out, "  \x1b[32m+ {value}\x1b[0m").unwrap();
×
254
                }
×
255
            }
256
        }
257

258
        out
13✔
259
    }
13✔
260

261
    fn check(&mut self, left: Peek<'_, '_>, right: Peek<'_, '_>) -> CheckResult {
99✔
262
        // Handle Option BEFORE innermost_peek (since Option's try_borrow_inner fails)
263
        if matches!(left.shape().def, Def::Option(_)) && matches!(right.shape().def, Def::Option(_))
99✔
264
        {
265
            return self.check_options(left, right);
2✔
266
        }
97✔
267

268
        // Unwrap transparent wrappers (like NonZero, newtype wrappers)
269
        let left = left.innermost_peek();
97✔
270
        let right = right.innermost_peek();
97✔
271

272
        // Try scalar comparison first (for leaf values like String, i32, etc.)
273
        // Scalars are compared by their formatted representation, except for floats
274
        // with tolerance configured.
275
        if matches!(left.shape().def, Def::Scalar) && matches!(right.shape().def, Def::Scalar) {
97✔
276
            // Try float comparison with tolerance if configured
277
            if self.options.float_tolerance.is_some() {
40✔
278
                if let Some((left_f64, right_f64)) = self.extract_floats(left, right) {
15✔
279
                    if self.floats_equal(left_f64, right_f64) {
14✔
280
                        return CheckResult::Same;
13✔
281
                    } else {
282
                        self.record_changed(left, right);
1✔
283
                        return CheckResult::Different;
1✔
284
                    }
285
                }
1✔
286
            }
25✔
287

288
            // Default: compare by formatted representation
289
            let left_str = Self::format_value(left);
26✔
290
            let right_str = Self::format_value(right);
26✔
291
            if left_str == right_str {
26✔
292
                return CheckResult::Same;
19✔
293
            } else {
294
                self.record_changed(left, right);
7✔
295
                return CheckResult::Different;
7✔
296
            }
297
        }
57✔
298

299
        // Try to compare structurally based on type/def
300
        // Note: Many types are UserType::Opaque but still have a useful Def (like Vec -> Def::List)
301
        // So we check Def first before giving up on Opaque types.
302

303
        // Handle lists/arrays/slices (Vec is Opaque but has Def::List)
304
        if left.into_list_like().is_ok() && right.into_list_like().is_ok() {
57✔
305
            return self.check_lists(left, right);
4✔
306
        }
53✔
307

308
        // Handle maps
309
        if matches!(left.shape().def, Def::Map(_)) && matches!(right.shape().def, Def::Map(_)) {
53✔
310
            return self.check_maps(left, right);
×
311
        }
53✔
312

313
        // Handle smart pointers
314
        if matches!(left.shape().def, Def::Pointer(_))
53✔
315
            && matches!(right.shape().def, Def::Pointer(_))
1✔
316
        {
317
            return self.check_pointers(left, right);
1✔
318
        }
52✔
319

320
        // Handle structs
321
        if let (Type::User(UserType::Struct(_)), Type::User(UserType::Struct(_))) =
322
            (left.shape().ty, right.shape().ty)
52✔
323
        {
324
            return self.check_structs(left, right);
9✔
325
        }
43✔
326

327
        // Handle enums
328
        if let (Type::User(UserType::Enum(_)), Type::User(UserType::Enum(_))) =
329
            (left.shape().ty, right.shape().ty)
43✔
330
        {
331
            return self.check_enums(left, right);
×
332
        }
43✔
333

334
        // Handle dynamic values (like facet_value::Value) - compare based on their runtime kind
335
        // This allows comparing Value against concrete types (e.g., Value array vs Vec)
336
        if let Def::DynamicValue(_) = left.shape().def {
43✔
337
            return self.check_with_dynamic_value(left, right);
42✔
338
        }
1✔
339
        if let Def::DynamicValue(_) = right.shape().def {
1✔
340
            return self.check_with_dynamic_value(right, left);
1✔
341
        }
×
342

343
        // At this point, if either is Opaque and we haven't handled it above, fail
344
        if matches!(left.shape().ty, Type::User(UserType::Opaque)) {
×
345
            return CheckResult::Opaque {
×
346
                type_name: left.shape().type_identifier,
×
347
            };
×
348
        }
×
349
        if matches!(right.shape().ty, Type::User(UserType::Opaque)) {
×
350
            return CheckResult::Opaque {
×
351
                type_name: right.shape().type_identifier,
×
352
            };
×
353
        }
×
354

355
        // Fallback: format and compare
356
        let left_str = Self::format_value(left);
×
357
        let right_str = Self::format_value(right);
×
358
        if left_str == right_str {
×
359
            CheckResult::Same
×
360
        } else {
361
            self.record_changed(left, right);
×
362
            CheckResult::Different
×
363
        }
364
    }
99✔
365

366
    fn check_structs(&mut self, left: Peek<'_, '_>, right: Peek<'_, '_>) -> CheckResult {
9✔
367
        let left_struct = left.into_struct().unwrap();
9✔
368
        let right_struct = right.into_struct().unwrap();
9✔
369

370
        let mut any_different = false;
9✔
371
        let mut seen_fields = std::collections::HashSet::new();
9✔
372

373
        // Check all fields in left
374
        for (field, left_value) in left_struct.fields() {
18✔
375
            seen_fields.insert(field.name);
18✔
376
            self.path.push(PathSegment::Field(field.name));
18✔
377

378
            if let Ok(right_value) = right_struct.field_by_name(field.name) {
18✔
379
                match self.check(left_value, right_value) {
18✔
380
                    CheckResult::Same => {}
16✔
381
                    CheckResult::Different => any_different = true,
2✔
382
                    opaque @ CheckResult::Opaque { .. } => {
×
383
                        self.path.pop();
×
384
                        return opaque;
×
385
                    }
386
                }
387
            } else {
×
388
                // Field only in left
×
389
                self.record_only_left(left_value);
×
390
                any_different = true;
×
391
            }
×
392

393
            self.path.pop();
18✔
394
        }
395

396
        // Check fields only in right
397
        for (field, right_value) in right_struct.fields() {
18✔
398
            if !seen_fields.contains(field.name) {
18✔
399
                self.path.push(PathSegment::Field(field.name));
×
400
                self.record_only_right(right_value);
×
401
                any_different = true;
×
402
                self.path.pop();
×
403
            }
18✔
404
        }
405

406
        if any_different {
9✔
407
            CheckResult::Different
2✔
408
        } else {
409
            CheckResult::Same
7✔
410
        }
411
    }
9✔
412

413
    fn check_enums(&mut self, left: Peek<'_, '_>, right: Peek<'_, '_>) -> CheckResult {
×
414
        let left_enum = left.into_enum().unwrap();
×
415
        let right_enum = right.into_enum().unwrap();
×
416

417
        let left_variant = left_enum.active_variant().unwrap();
×
418
        let right_variant = right_enum.active_variant().unwrap();
×
419

420
        // Different variants = different
421
        if left_variant.name != right_variant.name {
×
422
            self.record_changed(left, right);
×
423
            return CheckResult::Different;
×
424
        }
×
425

426
        // Same variant - check fields
427
        self.path.push(PathSegment::Variant(left_variant.name));
×
428

429
        let mut any_different = false;
×
430
        let mut seen_fields = std::collections::HashSet::new();
×
431

432
        for (field, left_value) in left_enum.fields() {
×
433
            seen_fields.insert(field.name);
×
434
            self.path.push(PathSegment::Field(field.name));
×
435

436
            if let Ok(Some(right_value)) = right_enum.field_by_name(field.name) {
×
437
                match self.check(left_value, right_value) {
×
438
                    CheckResult::Same => {}
×
439
                    CheckResult::Different => any_different = true,
×
440
                    opaque @ CheckResult::Opaque { .. } => {
×
441
                        self.path.pop();
×
442
                        self.path.pop();
×
443
                        return opaque;
×
444
                    }
445
                }
446
            } else {
×
447
                self.record_only_left(left_value);
×
448
                any_different = true;
×
449
            }
×
450

451
            self.path.pop();
×
452
        }
453

454
        for (field, right_value) in right_enum.fields() {
×
455
            if !seen_fields.contains(field.name) {
×
456
                self.path.push(PathSegment::Field(field.name));
×
457
                self.record_only_right(right_value);
×
458
                any_different = true;
×
459
                self.path.pop();
×
460
            }
×
461
        }
462

463
        self.path.pop();
×
464

465
        if any_different {
×
466
            CheckResult::Different
×
467
        } else {
468
            CheckResult::Same
×
469
        }
470
    }
×
471

472
    fn check_options(&mut self, left: Peek<'_, '_>, right: Peek<'_, '_>) -> CheckResult {
2✔
473
        let left_opt = left.into_option().unwrap();
2✔
474
        let right_opt = right.into_option().unwrap();
2✔
475

476
        match (left_opt.value(), right_opt.value()) {
2✔
477
            (None, None) => CheckResult::Same,
1✔
478
            (Some(l), Some(r)) => self.check(l, r),
1✔
479
            (Some(_), None) | (None, Some(_)) => {
480
                self.record_changed(left, right);
×
481
                CheckResult::Different
×
482
            }
483
        }
484
    }
2✔
485

486
    fn check_lists(&mut self, left: Peek<'_, '_>, right: Peek<'_, '_>) -> CheckResult {
4✔
487
        let left_list = left.into_list_like().unwrap();
4✔
488
        let right_list = right.into_list_like().unwrap();
4✔
489

490
        let left_items: Vec<_> = left_list.iter().collect();
4✔
491
        let right_items: Vec<_> = right_list.iter().collect();
4✔
492

493
        let mut any_different = false;
4✔
494
        let min_len = left_items.len().min(right_items.len());
4✔
495

496
        // Compare common elements
497
        for i in 0..min_len {
12✔
498
            self.path.push(PathSegment::Index(i));
12✔
499

500
            match self.check(left_items[i], right_items[i]) {
12✔
501
                CheckResult::Same => {}
8✔
502
                CheckResult::Different => any_different = true,
4✔
503
                opaque @ CheckResult::Opaque { .. } => {
×
504
                    self.path.pop();
×
505
                    return opaque;
×
506
                }
507
            }
508

509
            self.path.pop();
12✔
510
        }
511

512
        // Elements only in left (removed)
513
        for (i, item) in left_items.iter().enumerate().skip(min_len) {
4✔
514
            self.path.push(PathSegment::Index(i));
×
515
            self.record_only_left(*item);
×
516
            any_different = true;
×
517
            self.path.pop();
×
518
        }
×
519

520
        // Elements only in right (added)
521
        for (i, item) in right_items.iter().enumerate().skip(min_len) {
4✔
522
            self.path.push(PathSegment::Index(i));
×
523
            self.record_only_right(*item);
×
524
            any_different = true;
×
525
            self.path.pop();
×
526
        }
×
527

528
        if any_different {
4✔
529
            CheckResult::Different
2✔
530
        } else {
531
            CheckResult::Same
2✔
532
        }
533
    }
4✔
534

535
    fn check_maps(&mut self, left: Peek<'_, '_>, right: Peek<'_, '_>) -> CheckResult {
×
536
        let left_map = left.into_map().unwrap();
×
537
        let right_map = right.into_map().unwrap();
×
538

539
        let mut any_different = false;
×
540
        let mut seen_keys = std::collections::HashSet::new();
×
541

542
        for (left_key, left_value) in left_map.iter() {
×
543
            let key_str = Self::format_value(left_key);
×
544
            seen_keys.insert(key_str.clone());
×
545
            self.path.push(PathSegment::Key(key_str));
×
546

547
            // Try to find matching key in right map
548
            let mut found = false;
×
549
            for (right_key, right_value) in right_map.iter() {
×
550
                if Self::format_value(left_key) == Self::format_value(right_key) {
×
551
                    found = true;
×
552
                    match self.check(left_value, right_value) {
×
553
                        CheckResult::Same => {}
×
554
                        CheckResult::Different => any_different = true,
×
555
                        opaque @ CheckResult::Opaque { .. } => {
×
556
                            self.path.pop();
×
557
                            return opaque;
×
558
                        }
559
                    }
560
                    break;
×
561
                }
×
562
            }
563

564
            if !found {
×
565
                self.record_only_left(left_value);
×
566
                any_different = true;
×
567
            }
×
568

569
            self.path.pop();
×
570
        }
571

572
        // Check keys only in right
573
        for (right_key, right_value) in right_map.iter() {
×
574
            let key_str = Self::format_value(right_key);
×
575
            if !seen_keys.contains(&key_str) {
×
576
                self.path.push(PathSegment::Key(key_str));
×
577
                self.record_only_right(right_value);
×
578
                any_different = true;
×
579
                self.path.pop();
×
580
            }
×
581
        }
582

583
        if any_different {
×
584
            CheckResult::Different
×
585
        } else {
586
            CheckResult::Same
×
587
        }
588
    }
×
589

590
    fn check_pointers(&mut self, left: Peek<'_, '_>, right: Peek<'_, '_>) -> CheckResult {
1✔
591
        let left_ptr = left.into_pointer().unwrap();
1✔
592
        let right_ptr = right.into_pointer().unwrap();
1✔
593

594
        match (left_ptr.borrow_inner(), right_ptr.borrow_inner()) {
1✔
595
            (Some(left_inner), Some(right_inner)) => self.check(left_inner, right_inner),
1✔
596
            (None, None) => CheckResult::Same,
×
597
            _ => {
598
                self.record_changed(left, right);
×
599
                CheckResult::Different
×
600
            }
601
        }
602
    }
1✔
603

604
    /// Compare a DynamicValue (left) against any other Peek (right) based on the DynamicValue's runtime kind.
605
    /// This enables comparing e.g. `Value::Array` against `Vec<i32>`.
606
    fn check_with_dynamic_value(
43✔
607
        &mut self,
43✔
608
        dyn_peek: Peek<'_, '_>,
43✔
609
        other: Peek<'_, '_>,
43✔
610
    ) -> CheckResult {
43✔
611
        let dyn_val = dyn_peek.into_dynamic_value().unwrap();
43✔
612
        let kind = dyn_val.kind();
43✔
613

614
        match kind {
43✔
615
            DynValueKind::Null => {
616
                // Null compares equal to () or Option::None
617
                let other_str = Self::format_value(other);
×
618
                if other_str == "()" || other_str == "None" {
×
619
                    CheckResult::Same
×
620
                } else {
621
                    self.record_changed(dyn_peek, other);
×
622
                    CheckResult::Different
×
623
                }
624
            }
625
            DynValueKind::Bool => {
626
                // Compare against bool
627
                let dyn_bool = dyn_val.as_bool();
2✔
628

629
                // Check if other is also a DynamicValue bool
630
                let other_bool = if let Ok(other_dyn) = other.into_dynamic_value() {
2✔
631
                    other_dyn.as_bool()
×
632
                } else {
633
                    let other_str = Self::format_value(other);
2✔
634
                    match other_str.as_str() {
2✔
635
                        "true" => Some(true),
2✔
636
                        "false" => Some(false),
1✔
637
                        _ => None,
×
638
                    }
639
                };
640

641
                if dyn_bool == other_bool {
2✔
642
                    CheckResult::Same
1✔
643
                } else {
644
                    self.record_changed(dyn_peek, other);
1✔
645
                    CheckResult::Different
1✔
646
                }
647
            }
648
            DynValueKind::Number => {
649
                // Check if other is also a DynamicValue number
650
                if let Ok(other_dyn) = other.into_dynamic_value() {
26✔
651
                    // Compare DynamicValue numbers directly
652
                    let same = match (dyn_val.as_i64(), other_dyn.as_i64()) {
8✔
653
                        (Some(l), Some(r)) => l == r,
8✔
654
                        _ => match (dyn_val.as_u64(), other_dyn.as_u64()) {
×
655
                            (Some(l), Some(r)) => l == r,
×
656
                            _ => match (dyn_val.as_f64(), other_dyn.as_f64()) {
×
NEW
657
                                (Some(l), Some(r)) => self.floats_equal(l, r),
×
658
                                _ => false,
×
659
                            },
660
                        },
661
                    };
662
                    if same {
8✔
663
                        return CheckResult::Same;
7✔
664
                    } else {
665
                        self.record_changed(dyn_peek, other);
1✔
666
                        return CheckResult::Different;
1✔
667
                    }
668
                }
18✔
669

670
                // Compare against scalar number by parsing formatted value
671
                let other_str = Self::format_value(other);
18✔
672

673
                let same = if let Some(dyn_i64) = dyn_val.as_i64() {
18✔
674
                    other_str.parse::<i64>().ok() == Some(dyn_i64)
18✔
675
                } else if let Some(dyn_u64) = dyn_val.as_u64() {
×
676
                    other_str.parse::<u64>().ok() == Some(dyn_u64)
×
677
                } else if let Some(dyn_f64) = dyn_val.as_f64() {
×
NEW
678
                    other_str
×
NEW
679
                        .parse::<f64>()
×
NEW
680
                        .ok()
×
NEW
681
                        .is_some_and(|other_f64| self.floats_equal(dyn_f64, other_f64))
×
682
                } else {
683
                    false
×
684
                };
685

686
                if same {
18✔
687
                    CheckResult::Same
16✔
688
                } else {
689
                    self.record_changed(dyn_peek, other);
2✔
690
                    CheckResult::Different
2✔
691
                }
692
            }
693
            DynValueKind::String => {
694
                // Compare against string types
695
                let dyn_str = dyn_val.as_str();
4✔
696

697
                // Check if other is also a DynamicValue string
698
                let other_str = if let Ok(other_dyn) = other.into_dynamic_value() {
4✔
699
                    other_dyn.as_str()
2✔
700
                } else {
701
                    other.as_str()
2✔
702
                };
703

704
                if dyn_str == other_str {
4✔
705
                    CheckResult::Same
2✔
706
                } else {
707
                    self.record_changed(dyn_peek, other);
2✔
708
                    CheckResult::Different
2✔
709
                }
710
            }
711
            DynValueKind::Bytes => {
712
                // Compare against byte slice types
713
                let dyn_bytes = dyn_val.as_bytes();
×
714

715
                // Check if other is also a DynamicValue bytes
716
                let other_bytes = if let Ok(other_dyn) = other.into_dynamic_value() {
×
717
                    other_dyn.as_bytes()
×
718
                } else {
719
                    other.as_bytes()
×
720
                };
721

722
                if dyn_bytes == other_bytes {
×
723
                    CheckResult::Same
×
724
                } else {
725
                    self.record_changed(dyn_peek, other);
×
726
                    CheckResult::Different
×
727
                }
728
            }
729
            DynValueKind::Array => {
730
                // Compare against any list-like type (Vec, array, slice, or another DynamicValue array)
731
                self.check_dyn_array_against_other(dyn_peek, dyn_val, other)
9✔
732
            }
733
            DynValueKind::Object => {
734
                // Compare against maps or structs
735
                self.check_dyn_object_against_other(dyn_peek, dyn_val, other)
2✔
736
            }
737
            DynValueKind::DateTime => {
738
                // Compare datetime values by their components
739
                let dyn_dt = dyn_val.as_datetime();
×
740

741
                // Check if other is also a DynamicValue datetime
742
                let other_dt = if let Ok(other_dyn) = other.into_dynamic_value() {
×
743
                    other_dyn.as_datetime()
×
744
                } else {
745
                    None
×
746
                };
747

748
                if dyn_dt == other_dt {
×
749
                    CheckResult::Same
×
750
                } else {
751
                    self.record_changed(dyn_peek, other);
×
752
                    CheckResult::Different
×
753
                }
754
            }
755
        }
756
    }
43✔
757

758
    fn check_dyn_array_against_other(
9✔
759
        &mut self,
9✔
760
        dyn_peek: Peek<'_, '_>,
9✔
761
        dyn_val: facet_reflect::PeekDynamicValue<'_, '_>,
9✔
762
        other: Peek<'_, '_>,
9✔
763
    ) -> CheckResult {
9✔
764
        let dyn_len = dyn_val.array_len().unwrap_or(0);
9✔
765

766
        // Check if other is also a DynamicValue array
767
        if let Ok(other_dyn) = other.into_dynamic_value() {
9✔
768
            if other_dyn.kind() == DynValueKind::Array {
2✔
769
                let other_len = other_dyn.array_len().unwrap_or(0);
2✔
770
                return self
2✔
771
                    .check_two_dyn_arrays(dyn_peek, dyn_val, dyn_len, other, other_dyn, other_len);
2✔
772
            } else {
773
                self.record_changed(dyn_peek, other);
×
774
                return CheckResult::Different;
×
775
            }
776
        }
7✔
777

778
        // Check if other is list-like
779
        if let Ok(other_list) = other.into_list_like() {
7✔
780
            let other_len = other_list.len();
7✔
781
            let mut any_different = false;
7✔
782
            let min_len = dyn_len.min(other_len);
7✔
783

784
            // Compare common elements
785
            for i in 0..min_len {
18✔
786
                self.path.push(PathSegment::Index(i));
18✔
787

788
                if let (Some(dyn_elem), Some(other_elem)) =
18✔
789
                    (dyn_val.array_get(i), other_list.get(i))
18✔
790
                {
791
                    match self.check(dyn_elem, other_elem) {
18✔
792
                        CheckResult::Same => {}
17✔
793
                        CheckResult::Different => any_different = true,
1✔
794
                        opaque @ CheckResult::Opaque { .. } => {
×
795
                            self.path.pop();
×
796
                            return opaque;
×
797
                        }
798
                    }
799
                }
×
800

801
                self.path.pop();
18✔
802
            }
803

804
            // Elements only in dyn array
805
            for i in min_len..dyn_len {
7✔
806
                self.path.push(PathSegment::Index(i));
1✔
807
                if let Some(dyn_elem) = dyn_val.array_get(i) {
1✔
808
                    self.record_only_left(dyn_elem);
1✔
809
                    any_different = true;
1✔
810
                }
1✔
811
                self.path.pop();
1✔
812
            }
813

814
            // Elements only in other list
815
            for i in min_len..other_len {
7✔
816
                self.path.push(PathSegment::Index(i));
×
817
                if let Some(other_elem) = other_list.get(i) {
×
818
                    self.record_only_right(other_elem);
×
819
                    any_different = true;
×
820
                }
×
821
                self.path.pop();
×
822
            }
823

824
            if any_different {
7✔
825
                CheckResult::Different
2✔
826
            } else {
827
                CheckResult::Same
5✔
828
            }
829
        } else {
830
            // Other is not array-like, they're different
831
            self.record_changed(dyn_peek, other);
×
832
            CheckResult::Different
×
833
        }
834
    }
9✔
835

836
    fn check_two_dyn_arrays(
2✔
837
        &mut self,
2✔
838
        _left_peek: Peek<'_, '_>,
2✔
839
        left_dyn: facet_reflect::PeekDynamicValue<'_, '_>,
2✔
840
        left_len: usize,
2✔
841
        _right_peek: Peek<'_, '_>,
2✔
842
        right_dyn: facet_reflect::PeekDynamicValue<'_, '_>,
2✔
843
        right_len: usize,
2✔
844
    ) -> CheckResult {
2✔
845
        let mut any_different = false;
2✔
846
        let min_len = left_len.min(right_len);
2✔
847

848
        // Compare common elements
849
        for i in 0..min_len {
6✔
850
            self.path.push(PathSegment::Index(i));
6✔
851

852
            if let (Some(left_elem), Some(right_elem)) =
6✔
853
                (left_dyn.array_get(i), right_dyn.array_get(i))
6✔
854
            {
855
                match self.check(left_elem, right_elem) {
6✔
856
                    CheckResult::Same => {}
5✔
857
                    CheckResult::Different => any_different = true,
1✔
858
                    opaque @ CheckResult::Opaque { .. } => {
×
859
                        self.path.pop();
×
860
                        return opaque;
×
861
                    }
862
                }
863
            }
×
864

865
            self.path.pop();
6✔
866
        }
867

868
        // Elements only in left
869
        for i in min_len..left_len {
2✔
870
            self.path.push(PathSegment::Index(i));
×
871
            if let Some(left_elem) = left_dyn.array_get(i) {
×
872
                self.record_only_left(left_elem);
×
873
                any_different = true;
×
874
            }
×
875
            self.path.pop();
×
876
        }
877

878
        // Elements only in right
879
        for i in min_len..right_len {
2✔
880
            self.path.push(PathSegment::Index(i));
×
881
            if let Some(right_elem) = right_dyn.array_get(i) {
×
882
                self.record_only_right(right_elem);
×
883
                any_different = true;
×
884
            }
×
885
            self.path.pop();
×
886
        }
887

888
        if any_different {
2✔
889
            CheckResult::Different
1✔
890
        } else {
891
            CheckResult::Same
1✔
892
        }
893
    }
2✔
894

895
    fn check_dyn_object_against_other(
2✔
896
        &mut self,
2✔
897
        dyn_peek: Peek<'_, '_>,
2✔
898
        dyn_val: facet_reflect::PeekDynamicValue<'_, '_>,
2✔
899
        other: Peek<'_, '_>,
2✔
900
    ) -> CheckResult {
2✔
901
        let dyn_len = dyn_val.object_len().unwrap_or(0);
2✔
902

903
        // Check if other is also a DynamicValue object
904
        if let Ok(other_dyn) = other.into_dynamic_value() {
2✔
905
            if other_dyn.kind() == DynValueKind::Object {
2✔
906
                let other_len = other_dyn.object_len().unwrap_or(0);
2✔
907
                return self.check_two_dyn_objects(
2✔
908
                    dyn_peek, dyn_val, dyn_len, other, other_dyn, other_len,
2✔
909
                );
910
            } else {
911
                self.record_changed(dyn_peek, other);
×
912
                return CheckResult::Different;
×
913
            }
914
        }
×
915

916
        // Check if other is a map
917
        if let Ok(other_map) = other.into_map() {
×
918
            let mut any_different = false;
×
919
            let mut seen_keys = std::collections::HashSet::new();
×
920

921
            // Check all entries in dyn object
922
            for i in 0..dyn_len {
×
923
                if let Some((key, dyn_value)) = dyn_val.object_get_entry(i) {
×
924
                    seen_keys.insert(key.to_owned());
×
925
                    self.path.push(PathSegment::Key(key.to_owned()));
×
926

927
                    // Try to find key in map - need to compare by formatted key
928
                    let mut found = false;
×
929
                    for (map_key, map_value) in other_map.iter() {
×
930
                        if Self::format_value(map_key) == format!("{key:?}") {
×
931
                            found = true;
×
932
                            match self.check(dyn_value, map_value) {
×
933
                                CheckResult::Same => {}
×
934
                                CheckResult::Different => any_different = true,
×
935
                                opaque @ CheckResult::Opaque { .. } => {
×
936
                                    self.path.pop();
×
937
                                    return opaque;
×
938
                                }
939
                            }
940
                            break;
×
941
                        }
×
942
                    }
943

944
                    if !found {
×
945
                        self.record_only_left(dyn_value);
×
946
                        any_different = true;
×
947
                    }
×
948

949
                    self.path.pop();
×
950
                }
×
951
            }
952

953
            // Check keys only in map
954
            for (map_key, map_value) in other_map.iter() {
×
955
                let key_str = Self::format_value(map_key);
×
956
                // Remove quotes for comparison
957
                let key_unquoted = key_str.trim_matches('"');
×
958
                if !seen_keys.contains(key_unquoted) {
×
959
                    self.path.push(PathSegment::Key(key_unquoted.to_owned()));
×
960
                    self.record_only_right(map_value);
×
961
                    any_different = true;
×
962
                    self.path.pop();
×
963
                }
×
964
            }
965

966
            if any_different {
×
967
                CheckResult::Different
×
968
            } else {
969
                CheckResult::Same
×
970
            }
971
        } else if let Ok(other_struct) = other.into_struct() {
×
972
            // Compare DynamicValue object against struct fields
973
            let mut any_different = false;
×
974
            let mut seen_fields = std::collections::HashSet::new();
×
975

976
            // Check all entries in dyn object against struct fields
977
            for i in 0..dyn_len {
×
978
                if let Some((key, dyn_value)) = dyn_val.object_get_entry(i) {
×
979
                    seen_fields.insert(key.to_owned());
×
980
                    self.path.push(PathSegment::Key(key.to_owned()));
×
981

982
                    if let Ok(struct_value) = other_struct.field_by_name(key) {
×
983
                        match self.check(dyn_value, struct_value) {
×
984
                            CheckResult::Same => {}
×
985
                            CheckResult::Different => any_different = true,
×
986
                            opaque @ CheckResult::Opaque { .. } => {
×
987
                                self.path.pop();
×
988
                                return opaque;
×
989
                            }
990
                        }
991
                    } else {
×
992
                        self.record_only_left(dyn_value);
×
993
                        any_different = true;
×
994
                    }
×
995

996
                    self.path.pop();
×
997
                }
×
998
            }
999

1000
            // Check struct fields not in dyn object
1001
            for (field, struct_value) in other_struct.fields() {
×
1002
                if !seen_fields.contains(field.name) {
×
1003
                    self.path.push(PathSegment::Field(field.name));
×
1004
                    self.record_only_right(struct_value);
×
1005
                    any_different = true;
×
1006
                    self.path.pop();
×
1007
                }
×
1008
            }
1009

1010
            if any_different {
×
1011
                CheckResult::Different
×
1012
            } else {
1013
                CheckResult::Same
×
1014
            }
1015
        } else {
1016
            // Other is not object-like, they're different
1017
            self.record_changed(dyn_peek, other);
×
1018
            CheckResult::Different
×
1019
        }
1020
    }
2✔
1021

1022
    fn check_two_dyn_objects(
2✔
1023
        &mut self,
2✔
1024
        _left_peek: Peek<'_, '_>,
2✔
1025
        left_dyn: facet_reflect::PeekDynamicValue<'_, '_>,
2✔
1026
        left_len: usize,
2✔
1027
        _right_peek: Peek<'_, '_>,
2✔
1028
        right_dyn: facet_reflect::PeekDynamicValue<'_, '_>,
2✔
1029
        right_len: usize,
2✔
1030
    ) -> CheckResult {
2✔
1031
        let mut any_different = false;
2✔
1032
        let mut seen_keys = std::collections::HashSet::new();
2✔
1033

1034
        // Check all entries in left
1035
        for i in 0..left_len {
4✔
1036
            if let Some((key, left_value)) = left_dyn.object_get_entry(i) {
4✔
1037
                seen_keys.insert(key.to_owned());
4✔
1038
                self.path.push(PathSegment::Key(key.to_owned()));
4✔
1039

1040
                if let Some(right_value) = right_dyn.object_get(key) {
4✔
1041
                    match self.check(left_value, right_value) {
4✔
1042
                        CheckResult::Same => {}
3✔
1043
                        CheckResult::Different => any_different = true,
1✔
1044
                        opaque @ CheckResult::Opaque { .. } => {
×
1045
                            self.path.pop();
×
1046
                            return opaque;
×
1047
                        }
1048
                    }
1049
                } else {
×
1050
                    self.record_only_left(left_value);
×
1051
                    any_different = true;
×
1052
                }
×
1053

1054
                self.path.pop();
4✔
1055
            }
×
1056
        }
1057

1058
        // Check entries only in right
1059
        for i in 0..right_len {
4✔
1060
            if let Some((key, right_value)) = right_dyn.object_get_entry(i) {
4✔
1061
                if !seen_keys.contains(key) {
4✔
1062
                    self.path.push(PathSegment::Key(key.to_owned()));
×
1063
                    self.record_only_right(right_value);
×
1064
                    any_different = true;
×
1065
                    self.path.pop();
×
1066
                }
4✔
1067
            }
×
1068
        }
1069

1070
        if any_different {
2✔
1071
            CheckResult::Different
1✔
1072
        } else {
1073
            CheckResult::Same
1✔
1074
        }
1075
    }
2✔
1076
}
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