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}