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/// Check if two Facet values are structurally the same.
144///
145/// This does NOT require `PartialEq` - it walks the structure via reflection.
146/// Two values are "same" if they have the same structure and values, even if
147/// they have different type names.
148///
149/// Returns [`Sameness::Opaque`] if either value contains an opaque type.
150pub fn check_same<'f, T: Facet<'f>, U: Facet<'f>>(left: &T, right: &U) -> Sameness {
151    check_same_report(left, right).into_sameness()
152}
153
154/// Check if two Facet values are structurally the same, returning a detailed report.
155pub fn check_same_report<'f, 'mem, T: Facet<'f>, U: Facet<'f>>(
156    left: &'mem T,
157    right: &'mem U,
158) -> SameReport<'mem, 'f> {
159    check_same_with_report(left, right, SameOptions::default())
160}
161
162/// Check if two Facet values are structurally the same, with custom options.
163///
164/// Like [`check_same`], but allows configuring comparison behavior via [`SameOptions`].
165///
166/// # Example
167///
168/// ```
169/// use facet_assert::{check_same_with, SameOptions, Sameness};
170///
171/// let a = 1.0000001_f64;
172/// let b = 1.0000002_f64;
173///
174/// // With tolerance, these are considered the same
175/// let options = SameOptions::new().float_tolerance(1e-6);
176/// assert!(matches!(check_same_with(&a, &b, options), Sameness::Same));
177/// ```
178pub fn check_same_with<'f, T: Facet<'f>, U: Facet<'f>>(
179    left: &T,
180    right: &U,
181    options: SameOptions,
182) -> Sameness {
183    check_same_with_report(left, right, options).into_sameness()
184}
185
186/// Detailed comparison with custom options.
187pub fn check_same_with_report<'f, 'mem, T: Facet<'f>, U: Facet<'f>>(
188    left: &'mem T,
189    right: &'mem U,
190    options: SameOptions,
191) -> SameReport<'mem, 'f> {
192    let left_peek = Peek::new(left);
193    let right_peek = Peek::new(right);
194
195    // Convert SameOptions to DiffOptions
196    let mut diff_options = DiffOptions::new();
197    if let Some(tol) = options.float_tolerance {
198        diff_options = diff_options.with_float_tolerance(tol);
199    }
200    if let Some(threshold) = options.similarity_threshold {
201        diff_options = diff_options.with_similarity_threshold(threshold);
202    }
203
204    // Compute diff with options applied during computation
205    let diff = diff_new_peek_with_options(left_peek, right_peek, &diff_options);
206
207    if diff.is_equal() {
208        SameReport::Same
209    } else {
210        let mut report = DiffReport::new(diff, left_peek, right_peek);
211        if let Some(tol) = options.float_tolerance {
212            report = report.with_float_tolerance(tol);
213        }
214        SameReport::Different(Box::new(report))
215    }
216}