cmp/
lib.rs

1//! A macro to compare structs, field by field.
2//!
3//! This crate provides the `compare_structs!` macro, which is useful for writing
4//! `assert!`-style tests on structs. It provides more detailed output than a
5//! standard `assert_eq!` on two struct instances.
6//!
7//! # Basic Usage
8//!
9//! To compare specific fields between two structs, provide the struct expressions
10//! and the identifiers of the fields to compare.
11//!
12//! ```edition2024
13//! use cmp::compare_structs;
14//! # struct A { a: i32, b: &'static str }
15//! # struct B { a: i32, b: &'static str }
16//! let struct_a = A { a: 1, b: "hello" };
17//! let struct_b = B { a: 1, b: "world" };
18//!
19//! // This will pass, as we only compare the `a` field.
20//! compare_structs!(struct_a, struct_b, a);
21//! ```
22//!
23//! # `serde` feature
24//!
25//! This crate has an optional `serde` feature that allows comparing all fields
26//! of a struct without specifying them. To use it, enable the feature in your
27//! `Cargo.toml`:
28//!
29//! ```toml
30//! [dependencies]
31//! cmp = { version = "1.0.0", features = ["serde"] }
32//! ```
33//!
34//! With the `serde` feature enabled, you can call `compare_structs!` with just
35//! two arguments. The structs must derive `serde::Serialize`.
36//!
37//! ```edition2024
38//! # #[cfg(feature = "serde")]
39//! # {
40//! use cmp::compare_structs;
41//! use serde::Serialize;
42//!
43//! #[derive(Serialize)]
44//! struct MyStruct {
45//!     field1: i32,
46//!     field2: String,
47//! }
48//!
49//! let a = MyStruct { field1: 1, field2: "test".to_string() };
50//! let b = MyStruct { field1: 1, field2: "test".to_string() };
51//!
52//! // Compares all fields
53//! compare_structs!(a, b);
54//! # }
55//! ```
56//!
57//! If there are missing fields in one of the expressions when using the `serde`
58//! feature, the macro will panic with a clear error message indicating which
59//! field is missing from which struct.
60
61/// Macro which is mostly useful when writing `assert!` tests on structs.
62///
63/// ```edition2024
64/// use cmp::compare_structs;
65/// # struct A<'a> {
66/// #     a: i32,
67/// #     b: &'a str,
68/// #     c: [(f64, f32); 2],
69/// # }
70/// # struct B<'a> {
71/// #     a: i32,
72/// #     b: &'a str,
73/// #     c: [(f64, f32); 2],
74/// # }
75/// let struct_a = A {
76///     a: 10,
77///     b: "str",
78///     c: [(1.0, 1.0), (2.0, 2.0)],
79/// };
80/// let struct_b = B {
81///     a: 10,
82///     b: "diff str",
83///     c: [(1.0, 1.0), (2.0, 2.0)],
84/// };
85/// compare_structs!(struct_a, struct_b, a, c);
86/// ```
87///
88/// Output singles-out fields in the struct which do not match:
89///
90/// ```bash
91/// thread 'tests::compare_different_structs' panicked at src/lib.rs:135:9:
92/// c: [
93///     (
94///         1.0,
95///         1.0,
96///     ),
97///     (
98///         2.0,
99///         3.0,
100///     ),
101/// ] != [
102///     (
103///         1.0,
104///         1.0,
105///     ),
106///     (
107///         2.0,
108///         2.0,
109///     ),
110/// ]
111///
112/// note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
113/// ```
114///
115/// The main motivation behind this macro is for structs with many fields, where `assert_eq!(struct_a, struct_b)`'s output is difficult to read.
116///
117/// /// # Panics
118///
119/// Panics if any of the fields do not have partial equality.
120#[cfg(not(feature = "serde"))]
121#[macro_export]
122macro_rules! compare_structs {
123    ($expected:expr, $actual:expr, $($field:ident),+) => {{
124        let mut diffs = String::new();
125        $(
126            if $expected.$field != $actual.$field {
127                diffs.push_str(&format!(
128                    "{}: {:#?} != {:#?}\n",
129                    stringify!($field),
130                    $expected.$field,
131                    $actual.$field
132                ));
133            }
134        )+
135
136        assert!(diffs.is_empty(), "{diffs}");
137    }};
138}
139
140#[cfg(feature = "serde")]
141#[macro_export]
142macro_rules! compare_structs {
143    ($expected:expr, $actual:expr) => {{
144        let expected_val =
145            serde_json::to_value(&$expected).expect("Could not serialize expected value");
146        let actual_val = serde_json::to_value(&$actual).expect("Could not serialize actual value");
147
148        if expected_val != actual_val {
149            let expected_map = expected_val
150                .as_object()
151                .expect("Expected value is not an object");
152            let actual_map = actual_val
153                .as_object()
154                .expect("Actual value is not an object");
155            let mut diffs = String::new();
156
157            for (key, expected_field_val) in expected_map {
158                match actual_map.get(key) {
159                    Some(actual_field_val) => {
160                        if expected_field_val != actual_field_val {
161                            diffs.push_str(&format!(
162                                "{}: {:#?} != {:#?}\n",
163                                key, expected_field_val, actual_field_val
164                            ));
165                        }
166                    }
167                    None => {
168                        diffs.push_str(&format!(
169                            "{}: field missing from actual: {:#?}\n",
170                            key, expected_field_val
171                        ));
172                    }
173                }
174            }
175
176            for (key, actual_field_val) in actual_map {
177                if !expected_map.contains_key(key) {
178                    diffs.push_str(&format!(
179                        "{}: field missing from expected: {:#?}\n",
180                        key, actual_field_val
181                    ));
182                }
183            }
184
185            assert!(diffs.is_empty(), "{diffs}");
186        }
187    }};
188    ($expected:expr, $actual:expr, $($field:ident),+) => {{
189        let mut diffs = String::new();
190        $(
191            if $expected.$field != $actual.$field {
192                diffs.push_str(&format!(
193                    "{}: {:#?} != {:#?}\n",
194                    stringify!($field),
195                    $expected.$field,
196                    $actual.$field
197                ));
198            }
199        )+
200
201        assert!(diffs.is_empty(), "{diffs}");
202    }};
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    #[cfg(feature = "serde")]
209    use serde::Serialize;
210
211    #[cfg_attr(feature = "serde", derive(Serialize))]
212    struct A<'a> {
213        a: i32,
214        b: &'a str,
215        c: [(f64, f32); 2],
216    }
217
218    #[cfg_attr(feature = "serde", derive(Serialize))]
219    struct B<'a> {
220        a: i32,
221        b: &'a str,
222        c: [(f64, f32); 2],
223    }
224
225    static STRUCT_A: A = A {
226        a: 10,
227        b: "str",
228        c: [(1.0, 1.0), (2.0, 2.0)],
229    };
230
231    static STRUCT_B: B = B {
232        a: 10,
233        b: "str",
234        c: [(1.0, 1.0), (2.0, 2.0)],
235    };
236
237    #[test]
238    #[cfg(feature = "serde")]
239    fn compare_all_fields_no_args() {
240        let struct_a = A {
241            a: 10,
242            b: "str",
243            c: [(1.0, 1.0), (2.0, 2.0)],
244        };
245
246        let struct_b = B {
247            a: 10,
248            b: "str",
249            c: [(1.0, 1.0), (2.0, 2.0)],
250        };
251
252        compare_structs!(struct_a, struct_b);
253    }
254
255    #[test]
256    #[should_panic]
257    #[cfg(feature = "serde")]
258    fn compare_all_fields_no_args_panic() {
259        let struct_a = A {
260            a: 10,
261            b: "str",
262            c: [(1.0, 1.0), (2.0, 2.0)],
263        };
264
265        let struct_b = B {
266            a: 10,
267            b: "different",
268            c: [(1.0, 1.0), (2.0, 2.0)],
269        };
270
271        compare_structs!(struct_a, struct_b);
272    }
273
274    #[test]
275    fn compare_all_fields() {
276        let struct_a = A {
277            a: 10,
278            b: "str",
279            c: [(1.0, 1.0), (2.0, 2.0)],
280        };
281
282        let struct_b = B {
283            a: 10,
284            b: "str",
285            c: [(1.0, 1.0), (2.0, 2.0)],
286        };
287
288        compare_structs!(STRUCT_A, STRUCT_B, a, b, c);
289        compare_structs!(struct_a, struct_b, a, b, c);
290    }
291
292    #[test]
293    fn compare_some_fields() {
294        let struct_a = A {
295            a: 10,
296            b: "str",
297            c: [(1.0, 1.0), (2.0, 2.0)],
298        };
299        let struct_b = B {
300            a: 10,
301            b: "diff str",
302            c: [(1.0, 1.0), (2.0, 2.0)],
303        };
304
305        compare_structs!(struct_a, struct_b, a, c);
306    }
307
308    #[test]
309    fn compare_same_struct() {
310        let struct_a = A {
311            a: 10,
312            b: "str",
313            c: [(1.0, 1.0), (2.1, 2.0)],
314        };
315
316        let struct_a_again = A {
317            a: 10,
318            b: "str",
319            c: [(1.0, 1.0), (2.1, 2.0)],
320        };
321
322        compare_structs!(struct_a, struct_a_again, a, b, c);
323    }
324
325    #[test]
326    #[should_panic]
327    fn compare_different_structs() {
328        let struct_a = A {
329            a: 10,
330            b: "str",
331            c: [(1.0, 1.0), (2.0, 3.0)],
332        };
333
334        let struct_b = B {
335            a: 10,
336            b: "str",
337            c: [(1.0, 1.0), (2.0, 2.0)],
338        };
339
340        compare_structs!(struct_a, struct_b, a, b, c);
341    }
342}