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}