Skip to main content

facet_assert/
same.rs

1//! Structural sameness checking for Facet types.
2
3use facet_core::Facet;
4use facet_diff::{DiffOptions, DiffReport, diff_new_peek_with_options};
5use facet_reflect::Peek;
6
7/// Options for customizing structural comparison behavior.
8///
9/// Use the builder pattern to configure options:
10///
11/// ```
12/// use facet_assert::SameOptions;
13///
14/// let options = SameOptions::new()
15///     .float_tolerance(1e-6);
16/// ```
17#[derive(Debug, Clone, Default)]
18pub struct SameOptions {
19    /// Tolerance for floating-point comparisons.
20    /// If set, two floats are considered equal if their absolute difference
21    /// is less than or equal to this value.
22    float_tolerance: Option<f64>,
23}
24
25impl SameOptions {
26    /// Create a new `SameOptions` with default settings (exact comparison).
27    pub fn new() -> Self {
28        Self::default()
29    }
30
31    /// Set the tolerance for floating-point comparisons.
32    ///
33    /// When set, two `f32` or `f64` values are considered equal if:
34    /// `|left - right| <= tolerance`
35    ///
36    /// # Example
37    ///
38    /// ```
39    /// use facet_assert::{assert_same_with, SameOptions};
40    ///
41    /// let a = 1.0000001_f64;
42    /// let b = 1.0000002_f64;
43    ///
44    /// // This would fail with exact comparison:
45    /// // assert_same!(a, b);
46    ///
47    /// // But passes with tolerance:
48    /// assert_same_with!(a, b, SameOptions::new().float_tolerance(1e-6));
49    /// ```
50    pub const fn float_tolerance(mut self, tolerance: f64) -> Self {
51        self.float_tolerance = Some(tolerance);
52        self
53    }
54}
55
56/// Result of checking if two values are structurally the same.
57pub enum Sameness {
58    /// The values are structurally the same.
59    Same,
60    /// The values differ - contains a formatted diff.
61    Different(String),
62    /// Encountered an opaque type that cannot be compared.
63    Opaque {
64        /// The type name of the opaque type.
65        type_name: &'static str,
66    },
67}
68
69/// Detailed comparison result that retains the computed diff.
70pub enum SameReport<'mem, 'facet> {
71    /// The values are structurally the same.
72    Same,
73    /// The values differ - includes a diff report that can be rendered in multiple formats.
74    Different(Box<DiffReport<'mem, 'facet>>),
75    /// Encountered an opaque type that cannot be compared.
76    Opaque {
77        /// The type name of the opaque type.
78        type_name: &'static str,
79    },
80}
81
82impl<'mem, 'facet> SameReport<'mem, 'facet> {
83    /// Returns `true` if the two values matched.
84    pub const fn is_same(&self) -> bool {
85        matches!(self, Self::Same)
86    }
87
88    /// Convert this report into a [`Sameness`] summary, formatting diffs using the legacy display.
89    pub fn into_sameness(self) -> Sameness {
90        match self {
91            SameReport::Same => Sameness::Same,
92            SameReport::Different(report) => Sameness::Different(report.legacy_string()),
93            SameReport::Opaque { type_name } => Sameness::Opaque { type_name },
94        }
95    }
96
97    /// Get the diff report if the values were different.
98    pub fn diff(&self) -> Option<&DiffReport<'mem, 'facet>> {
99        match self {
100            SameReport::Different(report) => Some(report.as_ref()),
101            _ => None,
102        }
103    }
104}
105
106// =============================================================================
107// Same-type comparison (the common case)
108// =============================================================================
109
110/// Check if two Facet values are structurally the same.
111///
112/// This does NOT require `PartialEq` - it walks the structure via reflection.
113/// Both values must have the same type, which enables type inference to flow
114/// between arguments.
115///
116/// # Example
117///
118/// ```
119/// use facet_assert::check_same;
120///
121/// let x: Option<Option<i32>> = Some(None);
122/// check_same(&x, &Some(None)); // Type of Some(None) inferred from x
123/// ```
124///
125/// For comparing values of different types, use [`check_sameish`].
126pub fn check_same<'f, T: Facet<'f>>(left: &T, right: &T) -> Sameness {
127    check_same_report(left, right).into_sameness()
128}
129
130/// Check if two Facet values are structurally the same, returning a detailed report.
131pub fn check_same_report<'f, 'mem, T: Facet<'f>>(
132    left: &'mem T,
133    right: &'mem T,
134) -> SameReport<'mem, 'f> {
135    check_same_with_report(left, right, SameOptions::default())
136}
137
138/// Check if two Facet values are structurally the same, with custom options.
139///
140/// # Example
141///
142/// ```
143/// use facet_assert::{check_same_with, SameOptions, Sameness};
144///
145/// let a = 1.0000001_f64;
146/// let b = 1.0000002_f64;
147///
148/// // With tolerance, these are considered the same
149/// let options = SameOptions::new().float_tolerance(1e-6);
150/// assert!(matches!(check_same_with(&a, &b, options), Sameness::Same));
151/// ```
152pub fn check_same_with<'f, T: Facet<'f>>(left: &T, right: &T, options: SameOptions) -> Sameness {
153    check_same_with_report(left, right, options).into_sameness()
154}
155
156/// Detailed comparison with custom options.
157pub fn check_same_with_report<'f, 'mem, T: Facet<'f>>(
158    left: &'mem T,
159    right: &'mem T,
160    options: SameOptions,
161) -> SameReport<'mem, 'f> {
162    check_sameish_with_report(left, right, options)
163}
164
165// =============================================================================
166// Cross-type comparison (for migration scenarios, etc.)
167// =============================================================================
168
169/// Check if two Facet values of potentially different types are structurally the same.
170///
171/// Unlike [`check_same`], this allows comparing values of different types.
172/// Two values are "sameish" if they have the same structure and values,
173/// even if they have different type names.
174///
175/// **Note:** Because the two arguments can have different types, the compiler
176/// cannot infer types from one side to the other. If you get type inference
177/// errors, either add type annotations or use [`check_same`] instead.
178///
179/// # Example
180///
181/// ```
182/// use facet::Facet;
183/// use facet_assert::check_sameish;
184///
185/// #[derive(Facet)]
186/// struct PersonV1 { name: String }
187///
188/// #[derive(Facet)]
189/// struct PersonV2 { name: String }
190///
191/// let old = PersonV1 { name: "Alice".into() };
192/// let new = PersonV2 { name: "Alice".into() };
193/// check_sameish(&old, &new); // Different types, same structure
194/// ```
195pub fn check_sameish<'f, T: Facet<'f>, U: Facet<'f>>(left: &T, right: &U) -> Sameness {
196    check_sameish_report(left, right).into_sameness()
197}
198
199/// Check if two Facet values of different types are structurally the same, returning a detailed report.
200pub fn check_sameish_report<'f, 'mem, T: Facet<'f>, U: Facet<'f>>(
201    left: &'mem T,
202    right: &'mem U,
203) -> SameReport<'mem, 'f> {
204    check_sameish_with_report(left, right, SameOptions::default())
205}
206
207/// Check if two Facet values of different types are structurally the same, with custom options.
208pub fn check_sameish_with<'f, T: Facet<'f>, U: Facet<'f>>(
209    left: &T,
210    right: &U,
211    options: SameOptions,
212) -> Sameness {
213    check_sameish_with_report(left, right, options).into_sameness()
214}
215
216/// Detailed cross-type comparison with custom options.
217pub fn check_sameish_with_report<'f, 'mem, T: Facet<'f>, U: Facet<'f>>(
218    left: &'mem T,
219    right: &'mem U,
220    options: SameOptions,
221) -> SameReport<'mem, 'f> {
222    let left_peek = Peek::new(left);
223    let right_peek = Peek::new(right);
224
225    // Convert SameOptions to DiffOptions
226    let mut diff_options = DiffOptions::new();
227    if let Some(tol) = options.float_tolerance {
228        diff_options = diff_options.with_float_tolerance(tol);
229    }
230
231    // Compute diff with options applied during computation
232    let diff = diff_new_peek_with_options(left_peek, right_peek, &diff_options);
233
234    if diff.is_equal() {
235        SameReport::Same
236    } else {
237        let mut report = DiffReport::new(diff, left_peek, right_peek);
238        if let Some(tol) = options.float_tolerance {
239            report = report.with_float_tolerance(tol);
240        }
241        SameReport::Different(Box::new(report))
242    }
243}