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    /// Similarity threshold for tree-based element matching in sequences.
25    /// If set, sequence elements with structural similarity >= this threshold
26    /// are paired for inline diffing rather than shown as remove+add.
27    ///
28    /// This uses the cinereus GumTree algorithm to compute structural similarity
29    /// based on hash matching and Dice coefficient.
30    ///
31    /// Recommended values: 0.5-0.7. Higher = stricter matching.
32    /// When None (default), uses exact equality only.
33    similarity_threshold: Option<f64>,
34}
35
36impl SameOptions {
37    /// Create a new `SameOptions` with default settings (exact comparison).
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    /// Set the tolerance for floating-point comparisons.
43    ///
44    /// When set, two `f32` or `f64` values are considered equal if:
45    /// `|left - right| <= tolerance`
46    ///
47    /// # Example
48    ///
49    /// ```
50    /// use facet_assert::{assert_same_with, SameOptions};
51    ///
52    /// let a = 1.0000001_f64;
53    /// let b = 1.0000002_f64;
54    ///
55    /// // This would fail with exact comparison:
56    /// // assert_same!(a, b);
57    ///
58    /// // But passes with tolerance:
59    /// assert_same_with!(a, b, SameOptions::new().float_tolerance(1e-6));
60    /// ```
61    pub fn float_tolerance(mut self, tolerance: f64) -> Self {
62        self.float_tolerance = Some(tolerance);
63        self
64    }
65
66    /// Set the similarity threshold for tree-based element matching.
67    ///
68    /// When set, sequence elements with structural similarity >= this threshold
69    /// are paired for inline field-level diffing rather than shown as remove+add.
70    ///
71    /// This uses the cinereus GumTree algorithm to compute structural similarity
72    /// based on hash matching and Dice coefficient.
73    ///
74    /// # Arguments
75    /// * `threshold` - Minimum similarity score (0.0 to 1.0). Recommended: 0.5-0.7.
76    ///
77    /// # Example
78    ///
79    /// ```
80    /// use facet_assert::SameOptions;
81    ///
82    /// // Use tree-based similarity for sequence diffing
83    /// let options = SameOptions::new()
84    ///     .float_tolerance(0.001)
85    ///     .similarity_threshold(0.6);
86    /// ```
87    pub fn similarity_threshold(mut self, threshold: f64) -> Self {
88        self.similarity_threshold = Some(threshold);
89        self
90    }
91}
92
93/// Result of checking if two values are structurally the same.
94pub enum Sameness {
95    /// The values are structurally the same.
96    Same,
97    /// The values differ - contains a formatted diff.
98    Different(String),
99    /// Encountered an opaque type that cannot be compared.
100    Opaque {
101        /// The type name of the opaque type.
102        type_name: &'static str,
103    },
104}
105
106/// Detailed comparison result that retains the computed diff.
107pub enum SameReport<'mem, 'facet> {
108    /// The values are structurally the same.
109    Same,
110    /// The values differ - includes a diff report that can be rendered in multiple formats.
111    Different(Box<DiffReport<'mem, 'facet>>),
112    /// Encountered an opaque type that cannot be compared.
113    Opaque {
114        /// The type name of the opaque type.
115        type_name: &'static str,
116    },
117}
118
119impl<'mem, 'facet> SameReport<'mem, 'facet> {
120    /// Returns `true` if the two values matched.
121    pub fn is_same(&self) -> bool {
122        matches!(self, Self::Same)
123    }
124
125    /// Convert this report into a [`Sameness`] summary, formatting diffs using the legacy display.
126    pub fn into_sameness(self) -> Sameness {
127        match self {
128            SameReport::Same => Sameness::Same,
129            SameReport::Different(report) => Sameness::Different(report.legacy_string()),
130            SameReport::Opaque { type_name } => Sameness::Opaque { type_name },
131        }
132    }
133
134    /// Get the diff report if the values were different.
135    pub fn diff(&self) -> Option<&DiffReport<'mem, 'facet>> {
136        match self {
137            SameReport::Different(report) => Some(report.as_ref()),
138            _ => None,
139        }
140    }
141}
142
143// =============================================================================
144// Same-type comparison (the common case)
145// =============================================================================
146
147/// Check if two Facet values are structurally the same.
148///
149/// This does NOT require `PartialEq` - it walks the structure via reflection.
150/// Both values must have the same type, which enables type inference to flow
151/// between arguments.
152///
153/// # Example
154///
155/// ```
156/// use facet_assert::check_same;
157///
158/// let x: Option<Option<i32>> = Some(None);
159/// check_same(&x, &Some(None)); // Type of Some(None) inferred from x
160/// ```
161///
162/// For comparing values of different types, use [`check_sameish`].
163pub fn check_same<'f, T: Facet<'f>>(left: &T, right: &T) -> Sameness {
164    check_same_report(left, right).into_sameness()
165}
166
167/// Check if two Facet values are structurally the same, returning a detailed report.
168pub fn check_same_report<'f, 'mem, T: Facet<'f>>(
169    left: &'mem T,
170    right: &'mem T,
171) -> SameReport<'mem, 'f> {
172    check_same_with_report(left, right, SameOptions::default())
173}
174
175/// Check if two Facet values are structurally the same, with custom options.
176///
177/// # Example
178///
179/// ```
180/// use facet_assert::{check_same_with, SameOptions, Sameness};
181///
182/// let a = 1.0000001_f64;
183/// let b = 1.0000002_f64;
184///
185/// // With tolerance, these are considered the same
186/// let options = SameOptions::new().float_tolerance(1e-6);
187/// assert!(matches!(check_same_with(&a, &b, options), Sameness::Same));
188/// ```
189pub fn check_same_with<'f, T: Facet<'f>>(left: &T, right: &T, options: SameOptions) -> Sameness {
190    check_same_with_report(left, right, options).into_sameness()
191}
192
193/// Detailed comparison with custom options.
194pub fn check_same_with_report<'f, 'mem, T: Facet<'f>>(
195    left: &'mem T,
196    right: &'mem T,
197    options: SameOptions,
198) -> SameReport<'mem, 'f> {
199    check_sameish_with_report(left, right, options)
200}
201
202// =============================================================================
203// Cross-type comparison (for migration scenarios, etc.)
204// =============================================================================
205
206/// Check if two Facet values of potentially different types are structurally the same.
207///
208/// Unlike [`check_same`], this allows comparing values of different types.
209/// Two values are "sameish" if they have the same structure and values,
210/// even if they have different type names.
211///
212/// **Note:** Because the two arguments can have different types, the compiler
213/// cannot infer types from one side to the other. If you get type inference
214/// errors, either add type annotations or use [`check_same`] instead.
215///
216/// # Example
217///
218/// ```
219/// use facet::Facet;
220/// use facet_assert::check_sameish;
221///
222/// #[derive(Facet)]
223/// struct PersonV1 { name: String }
224///
225/// #[derive(Facet)]
226/// struct PersonV2 { name: String }
227///
228/// let old = PersonV1 { name: "Alice".into() };
229/// let new = PersonV2 { name: "Alice".into() };
230/// check_sameish(&old, &new); // Different types, same structure
231/// ```
232pub fn check_sameish<'f, T: Facet<'f>, U: Facet<'f>>(left: &T, right: &U) -> Sameness {
233    check_sameish_report(left, right).into_sameness()
234}
235
236/// Check if two Facet values of different types are structurally the same, returning a detailed report.
237pub fn check_sameish_report<'f, 'mem, T: Facet<'f>, U: Facet<'f>>(
238    left: &'mem T,
239    right: &'mem U,
240) -> SameReport<'mem, 'f> {
241    check_sameish_with_report(left, right, SameOptions::default())
242}
243
244/// Check if two Facet values of different types are structurally the same, with custom options.
245pub fn check_sameish_with<'f, T: Facet<'f>, U: Facet<'f>>(
246    left: &T,
247    right: &U,
248    options: SameOptions,
249) -> Sameness {
250    check_sameish_with_report(left, right, options).into_sameness()
251}
252
253/// Detailed cross-type comparison with custom options.
254pub fn check_sameish_with_report<'f, 'mem, T: Facet<'f>, U: Facet<'f>>(
255    left: &'mem T,
256    right: &'mem U,
257    options: SameOptions,
258) -> SameReport<'mem, 'f> {
259    let left_peek = Peek::new(left);
260    let right_peek = Peek::new(right);
261
262    // Convert SameOptions to DiffOptions
263    let mut diff_options = DiffOptions::new();
264    if let Some(tol) = options.float_tolerance {
265        diff_options = diff_options.with_float_tolerance(tol);
266    }
267    if let Some(threshold) = options.similarity_threshold {
268        diff_options = diff_options.with_similarity_threshold(threshold);
269    }
270
271    // Compute diff with options applied during computation
272    let diff = diff_new_peek_with_options(left_peek, right_peek, &diff_options);
273
274    if diff.is_equal() {
275        SameReport::Same
276    } else {
277        let mut report = DiffReport::new(diff, left_peek, right_peek);
278        if let Some(tol) = options.float_tolerance {
279            report = report.with_float_tolerance(tol);
280        }
281        SameReport::Different(Box::new(report))
282    }
283}