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}