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}