json_matcher/
macros.rs

1/// "Assert json matches"
2/// Asserts that the given JSON in the first argument matches the JSON matcher defined by the second argument.
3/// Panics if the JSON does not match any expectations. Will print out each error encountered as well as the actual JSON encountered.
4///
5/// ```
6/// use serde_json::json;
7/// use json_matcher::{assert_jm, AnyMatcher};
8///
9/// let test_data = json!({"name": "John", "age": 30});
10///
11/// // exact match against json defined in-line
12/// assert_jm!(test_data, { "name": "John", "age": 30 });
13///
14/// // can also use non-exact "matchers"
15/// assert_jm!(test_data, { "name": "John", "age": AnyMatcher::not_null() })
16/// ```
17#[macro_export]
18macro_rules! assert_jm {
19    // Handle object syntax directly
20    ($actual:expr, { $($json:tt)* }) => {{
21        let actual = &$actual;
22        let expectation = $crate::create_json_matcher!({ $($json)* });
23        let errors = $crate::JsonMatcher::json_matches(&expectation, &actual);
24        if !errors.is_empty() {
25            let bullets = errors
26                .into_iter()
27                .map(|e| format!("  - {}", e))
28                .collect::<Vec<String>>();
29            let error_message = format!("\nJson matcher failed:\n{}", bullets.join("\n"));
30            let actual_message = format!(
31                "Actual:\n{}",
32                serde_json::to_string_pretty(&actual).unwrap()
33            );
34            panic!("{}\n\n{}", error_message, actual_message);
35        }
36    }};
37
38    // Handle array syntax directly
39    ($actual:expr, [ $($json:tt)* ]) => {{
40        let actual = &$actual;
41        let expectation = $crate::create_json_matcher!([ $($json)* ]);
42        let errors = $crate::JsonMatcher::json_matches(&expectation, &actual);
43        if !errors.is_empty() {
44            let bullets = errors
45                .into_iter()
46                .map(|e| format!("  - {}", e))
47                .collect::<Vec<String>>();
48            let error_message = format!("\nJson matcher failed:\n{}", bullets.join("\n"));
49            let actual_message = format!(
50                "Actual:\n{}",
51                serde_json::to_string_pretty(&actual).unwrap()
52            );
53            panic!("{}\n\n{}", error_message, actual_message);
54        }
55    }};
56
57    // Handle literals directly
58    ($actual:expr, $literal:literal) => {{
59        let actual = &$actual;
60        let expectation = $crate::create_json_matcher!($literal);
61        let errors = $crate::JsonMatcher::json_matches(&expectation, &actual);
62        if !errors.is_empty() {
63            let bullets = errors
64                .into_iter()
65                .map(|e| format!("  - {}", e))
66                .collect::<Vec<String>>();
67            let error_message = format!("\nJson matcher failed:\n{}", bullets.join("\n"));
68            let actual_message = format!(
69                "Actual:\n{}",
70                serde_json::to_string_pretty(&actual).unwrap()
71            );
72            panic!("{}\n\n{}", error_message, actual_message);
73        }
74    }};
75
76    // Handle null
77    ($actual:expr, null) => {{
78        let actual = &$actual;
79        let expectation = $crate::create_json_matcher!(null);
80        let errors = $crate::JsonMatcher::json_matches(&expectation, &actual);
81        if !errors.is_empty() {
82            let bullets = errors
83                .into_iter()
84                .map(|e| format!("  - {}", e))
85                .collect::<Vec<String>>();
86            let error_message = format!("\nJson matcher failed:\n{}", bullets.join("\n"));
87            let actual_message = format!(
88                "Actual:\n{}",
89                serde_json::to_string_pretty(&actual).unwrap()
90            );
91            panic!("{}\n\n{}", error_message, actual_message);
92        }
93    }};
94
95    // Handle true
96    ($actual:expr, true) => {{
97        let actual = &$actual;
98        let expectation = $crate::create_json_matcher!(true);
99        let errors = $crate::JsonMatcher::json_matches(&expectation, &actual);
100        if !errors.is_empty() {
101            let bullets = errors
102                .into_iter()
103                .map(|e| format!("  - {}", e))
104                .collect::<Vec<String>>();
105            let error_message = format!("\nJson matcher failed:\n{}", bullets.join("\n"));
106            let actual_message = format!(
107                "Actual:\n{}",
108                serde_json::to_string_pretty(&actual).unwrap()
109            );
110            panic!("{}\n\n{}", error_message, actual_message);
111        }
112    }};
113
114    // Handle false
115    ($actual:expr, false) => {{
116        let actual = &$actual;
117        let expectation = $crate::create_json_matcher!(false);
118        let errors = $crate::JsonMatcher::json_matches(&expectation, &actual);
119        if !errors.is_empty() {
120            let bullets = errors
121                .into_iter()
122                .map(|e| format!("  - {}", e))
123                .collect::<Vec<String>>();
124            let error_message = format!("\nJson matcher failed:\n{}", bullets.join("\n"));
125            let actual_message = format!(
126                "Actual:\n{}",
127                serde_json::to_string_pretty(&actual).unwrap()
128            );
129            panic!("{}\n\n{}", error_message, actual_message);
130        }
131    }};
132
133    // Original syntax - when passed an expression (must be last)
134    ($actual:expr, $expectation:expr) => {{
135        let actual = &$actual;
136        let expectation = &$expectation;
137        let errors = $crate::JsonMatcher::json_matches(expectation, &actual);
138        if !errors.is_empty() {
139            let bullets = errors
140                .into_iter()
141                .map(|e| format!("  - {}", e))
142                .collect::<Vec<String>>();
143            let error_message = format!("\nJson matcher failed:\n{}", bullets.join("\n"));
144            let actual_message = format!(
145                "Actual:\n{}",
146                serde_json::to_string_pretty(&actual).unwrap()
147            );
148            panic!("{}\n\n{}", error_message, actual_message);
149        }
150    }};
151}
152
153/// Create a json matcher from JSON-like syntax with embedded matchers
154///
155/// ```
156/// use json_matcher::{
157///     create_json_matcher, BooleanMatcher, JsonMatcher, JsonMatcherError, JsonPath, JsonPathElement,
158/// };
159/// use serde_json::json;
160///
161/// let matcher = create_json_matcher!({
162///     "name": "John",
163///     "is_cool": BooleanMatcher::any()
164/// });
165///
166/// let test_data = json!({
167///     "name": "John",
168///     "is_cool": "unknown"
169/// });
170///
171/// assert_eq!(
172///     matcher.json_matches(&test_data),
173///     vec![JsonMatcherError {
174///         path: JsonPath::from(vec![
175///             JsonPathElement::Root,
176///             JsonPathElement::Key("is_cool".to_string())
177///         ]),
178///         message: "Value is not a boolean".to_string()
179///     }]
180/// );
181/// ```
182#[macro_export]
183macro_rules! create_json_matcher {
184    // Handle null
185    (null) => {
186        $crate::NullMatcher::new()
187    };
188
189    // Handle booleans
190    (true) => {
191        $crate::BooleanMatcher::exact(true)
192    };
193    (false) => {
194        $crate::BooleanMatcher::exact(false)
195    };
196
197    // Handle numbers (integers and floats)
198    ($num:literal) => {{
199        // We'll use serde_json::json! to parse the number and then convert
200        let value = serde_json::json!($num);
201        value
202    }};
203
204    // Handle strings
205    ($string:literal) => {
206        $crate::StringMatcher::new($string)
207    };
208
209    // Handle arrays
210    ([ $($item:tt),* $(,)? ]) => {
211        $crate::ArrayMatcher::new()
212            $(.element($crate::create_json_matcher!($item)))*
213    };
214
215    // Handle objects
216    ({ $($json:tt)* }) => {
217        $crate::create_json_matcher!(@object {} $($json)*)
218    };
219
220    // Internal rules for parsing object fields
221    // Handle empty object (no fields)
222    (@object {$($out:tt)*}) => {
223        $crate::ObjectMatcher::new() $($out)*
224    };
225    // Handle nested objects
226    (@object {$($out:tt)*} $key:literal : { $($value:tt)* } , $($rest:tt)*) => {
227        $crate::create_json_matcher!(@object {$($out)* .field($key, $crate::create_json_matcher!({ $($value)* }))} $($rest)*)
228    };
229    (@object {$($out:tt)*} $key:literal : { $($value:tt)* }) => {
230        $crate::ObjectMatcher::new() $($out)* .field($key, $crate::create_json_matcher!({ $($value)* }))
231    };
232    // Handle arrays
233    (@object {$($out:tt)*} $key:literal : [ $($value:tt)* ] , $($rest:tt)*) => {
234        $crate::create_json_matcher!(@object {$($out)* .field($key, $crate::create_json_matcher!([ $($value)* ]))} $($rest)*)
235    };
236    (@object {$($out:tt)*} $key:literal : [ $($value:tt)* ]) => {
237        $crate::ObjectMatcher::new() $($out)* .field($key, $crate::create_json_matcher!([ $($value)* ]))
238    };
239    // Handle null, true, false keywords (must come before literals)
240    (@object {$($out:tt)*} $key:literal : null , $($rest:tt)*) => {
241        $crate::create_json_matcher!(@object {$($out)* .field($key, $crate::create_json_matcher!(null))} $($rest)*)
242    };
243    (@object {$($out:tt)*} $key:literal : null) => {
244        $crate::ObjectMatcher::new() $($out)* .field($key, $crate::create_json_matcher!(null))
245    };
246    (@object {$($out:tt)*} $key:literal : true , $($rest:tt)*) => {
247        $crate::create_json_matcher!(@object {$($out)* .field($key, $crate::create_json_matcher!(true))} $($rest)*)
248    };
249    (@object {$($out:tt)*} $key:literal : true) => {
250        $crate::ObjectMatcher::new() $($out)* .field($key, $crate::create_json_matcher!(true))
251    };
252    (@object {$($out:tt)*} $key:literal : false , $($rest:tt)*) => {
253        $crate::create_json_matcher!(@object {$($out)* .field($key, $crate::create_json_matcher!(false))} $($rest)*)
254    };
255    (@object {$($out:tt)*} $key:literal : false) => {
256        $crate::ObjectMatcher::new() $($out)* .field($key, $crate::create_json_matcher!(false))
257    };
258    // Handle literals
259    (@object {$($out:tt)*} $key:literal : $value:literal , $($rest:tt)*) => {
260        $crate::create_json_matcher!(@object {$($out)* .field($key, $crate::create_json_matcher!($value))} $($rest)*)
261    };
262    (@object {$($out:tt)*} $key:literal : $value:literal) => {
263        $crate::ObjectMatcher::new() $($out)* .field($key, $crate::create_json_matcher!($value))
264    };
265    // Handle identifiers as keys with null, true, false
266    (@object {$($out:tt)*} $key:ident : null , $($rest:tt)*) => {
267        $crate::create_json_matcher!(@object {$($out)* .field(stringify!($key), $crate::create_json_matcher!(null))} $($rest)*)
268    };
269    (@object {$($out:tt)*} $key:ident : null) => {
270        $crate::ObjectMatcher::new() $($out)* .field(stringify!($key), $crate::create_json_matcher!(null))
271    };
272    (@object {$($out:tt)*} $key:ident : true , $($rest:tt)*) => {
273        $crate::create_json_matcher!(@object {$($out)* .field(stringify!($key), $crate::create_json_matcher!(true))} $($rest)*)
274    };
275    (@object {$($out:tt)*} $key:ident : true) => {
276        $crate::ObjectMatcher::new() $($out)* .field(stringify!($key), $crate::create_json_matcher!(true))
277    };
278    (@object {$($out:tt)*} $key:ident : false , $($rest:tt)*) => {
279        $crate::create_json_matcher!(@object {$($out)* .field(stringify!($key), $crate::create_json_matcher!(false))} $($rest)*)
280    };
281    (@object {$($out:tt)*} $key:ident : false) => {
282        $crate::ObjectMatcher::new() $($out)* .field(stringify!($key), $crate::create_json_matcher!(false))
283    };
284    // Handle identifiers as keys with literal values
285    (@object {$($out:tt)*} $key:ident : $value:literal , $($rest:tt)*) => {
286        $crate::create_json_matcher!(@object {$($out)* .field(stringify!($key), $crate::create_json_matcher!($value))} $($rest)*)
287    };
288    (@object {$($out:tt)*} $key:ident : $value:literal) => {
289        $crate::ObjectMatcher::new() $($out)* .field(stringify!($key), $crate::create_json_matcher!($value))
290    };
291    // Handle expressions (matchers, variables, etc.) - must come last as catch-all
292    (@object {$($out:tt)*} $key:literal : $value:expr , $($rest:tt)*) => {
293        $crate::create_json_matcher!(@object {$($out)* .field($key, $value)} $($rest)*)
294    };
295    (@object {$($out:tt)*} $key:literal : $value:expr) => {
296        $crate::ObjectMatcher::new() $($out)* .field($key, $value)
297    };
298    (@object {$($out:tt)*} $key:ident : $value:expr , $($rest:tt)*) => {
299        $crate::create_json_matcher!(@object {$($out)* .field(stringify!($key), $value)} $($rest)*)
300    };
301    (@object {$($out:tt)*} $key:ident : $value:expr) => {
302        $crate::ObjectMatcher::new() $($out)* .field(stringify!($key), $value)
303    };
304
305    // Handle expressions (for matcher types) - this must come last
306    ($expr:expr) => {
307        $expr
308    };
309}
310
311#[cfg(test)]
312mod tests {
313    use crate::{assert_jm, create_json_matcher, test::catch_string_panic};
314    use crate::{AnyMatcher, JsonMatcher};
315    use serde_json::json;
316
317    // Mock UuidMatcher for testing
318    struct UuidMatcher;
319
320    impl UuidMatcher {
321        fn new() -> Self {
322            Self
323        }
324    }
325
326    impl JsonMatcher for UuidMatcher {
327        fn json_matches(&self, value: &serde_json::Value) -> Vec<crate::JsonMatcherError> {
328            match value.as_str() {
329                Some(s) if s.len() == 36 && s.chars().filter(|&c| c == '-').count() == 4 => vec![],
330                Some(_) => vec![crate::JsonMatcherError::at_root(
331                    "Expected valid UUID format",
332                )],
333                None => vec![crate::JsonMatcherError::at_root("Expected string for UUID")],
334            }
335        }
336    }
337
338    #[test]
339    fn test_assert_jm_with_json_syntax_success() {
340        let actual = json!({
341            "name": "John",
342            "age": 30,
343            "active": true
344        });
345
346        // Should pass - exact match using direct JSON syntax
347        assert_jm!(actual, {
348            "name": "John",
349            "age": 30,
350            "active": true
351        });
352    }
353
354    #[test]
355    fn test_assert_jm_with_json_syntax_failure() {
356        // Should not pass
357        assert_eq!(
358            catch_string_panic(|| assert_jm!(json!({
359                "name": "John",
360                "age": 30,
361                "active": true
362            }), {
363                "name": "John",
364                "age": 35,
365                "active": true
366            })),
367            r#"
368Json matcher failed:
369  - $.age: Expected integer 35 but got 30
370
371Actual:
372{
373  "name": "John",
374  "age": 30,
375  "active": true
376}"#
377        );
378    }
379
380    #[test]
381    fn test_assert_jm_with_matcher_expression_success() {
382        let actual = json!({
383            "id": "550e8400-e29b-41d4-a716-446655440000",
384            "name": "John"
385        });
386
387        // Should pass - using embedded matcher
388        assert_jm!(actual, {
389            "id": UuidMatcher::new(),
390            "name": "John"
391        });
392    }
393
394    #[test]
395    fn test_assert_jm_with_matcher_expression_failure_on_nested_matched() {
396        // Should not pass
397        assert_eq!(
398            catch_string_panic(|| assert_jm!(json!({
399                "id": "bloop",
400                "name": "John"
401            }), {
402                "id": UuidMatcher::new(),
403                "name": "John"
404            })),
405            r#"
406Json matcher failed:
407  - $.id: Expected valid UUID format
408
409Actual:
410{
411  "id": "bloop",
412  "name": "John"
413}"#
414        );
415    }
416
417    #[test]
418    fn test_assert_jm_with_matcher_expression_failure_on_exact_match() {
419        // Should not pass
420        assert_eq!(
421            catch_string_panic(|| assert_jm!(json!({
422                "id": "550e8400-e29b-41d4-a716-446655440000",
423                "name": "Jim"
424            }), {
425                "id": UuidMatcher::new(),
426                "name": "John"
427            })),
428            r#"
429Json matcher failed:
430  - $.name: Expected string "John" but got "Jim"
431
432Actual:
433{
434  "id": "550e8400-e29b-41d4-a716-446655440000",
435  "name": "Jim"
436}"#
437        );
438    }
439
440    #[test]
441    fn test_assert_jm_with_matcher_expression_failure_on_both() {
442        // Should not pass
443        assert_eq!(
444            catch_string_panic(|| assert_jm!(json!({
445                "id": "bloop",
446                "name": "Jim"
447            }), {
448                "id": UuidMatcher::new(),
449                "name": "John"
450            })),
451            r#"
452Json matcher failed:
453  - $.id: Expected valid UUID format
454  - $.name: Expected string "John" but got "Jim"
455
456Actual:
457{
458  "id": "bloop",
459  "name": "Jim"
460}"#
461        );
462    }
463
464    #[test]
465    fn test_assert_jm_with_mixed_matchers() {
466        let actual = json!({
467            "id": "550e8400-e29b-41d4-a716-446655440000",
468            "name": "John",
469            "tags": ["admin", "user"],
470            "metadata": {
471                "created": "2023-01-01",
472                "version": 1
473            }
474        });
475
476        // Mix of exact values and matchers
477        assert_jm!(actual, {
478            "id": UuidMatcher::new(),
479            "name": "John",
480            "tags": ["admin", "user"],
481            "metadata": {
482                "created": AnyMatcher::new(),
483                "version": 1
484            }
485        });
486    }
487
488    #[test]
489    fn test_assert_jm_failure_message() {
490        let actual = json!({
491            "name": "Jane",
492            "age": 25
493        });
494
495        let error_message = catch_string_panic(|| {
496            assert_jm!(actual, {
497                "name": "John",
498                "age": 25
499            });
500        });
501
502        assert!(error_message.contains("Json matcher failed"));
503        assert!(error_message.contains("Expected string \"John\" but got \"Jane\""));
504    }
505
506    #[test]
507    fn test_create_json_matcher_macro_directly() {
508        let matcher = create_json_matcher!({
509            "field1": "exact value",
510            "field2": AnyMatcher::new()
511        });
512
513        let valid_json = json!({
514            "field1": "exact value",
515            "field2": "anything"
516        });
517
518        assert_eq!(matcher.json_matches(&valid_json), vec![]);
519    }
520
521    #[test]
522    fn test_assert_jm_with_arrays() {
523        let actual = json!({
524            "items": [1, 2, 3],
525            "names": ["Alice", "Bob"]
526        });
527
528        assert_jm!(actual, {
529            "items": [1, 2, 3],
530            "names": ["Alice", "Bob"]
531        });
532    }
533
534    #[test]
535    fn test_assert_jm_nested_objects() {
536        let actual = json!({
537            "user": {
538                "profile": {
539                    "name": "John",
540                    "verified": true
541                }
542            }
543        });
544
545        assert_jm!(actual, {
546            "user": {
547                "profile": {
548                    "name": "John",
549                    "verified": true
550                }
551            }
552        });
553    }
554
555    #[test]
556    fn test_assert_jm_original_syntax_still_works() {
557        let actual = json!({
558            "value": "test"
559        });
560
561        // Original syntax with expression still works
562        let matcher = create_json_matcher!({
563            "value": "test"
564        });
565        assert_jm!(actual, matcher);
566
567        // Can also use json! directly for exact matching
568        assert_jm!(actual, json!({"value": "test"}));
569    }
570
571    #[test]
572    fn test_assert_jm_direct_literals() {
573        // Test direct string literal
574        assert_jm!(json!("hello"), "hello");
575
576        // Test direct number literal
577        assert_jm!(json!(42), 42);
578
579        // Test direct boolean literals
580        assert_jm!(json!(true), true);
581        assert_jm!(json!(false), false);
582
583        // Test direct null
584        assert_jm!(json!(null), null);
585
586        // Test direct array
587        assert_jm!(json!([1, 2, 3]), [1, 2, 3]);
588    }
589
590    #[test]
591    fn test_empty_object() {
592        // Test empty object matching
593        assert_jm!(json!({}), {});
594
595        // Test nested empty object
596        assert_jm!(json!({"empty": {}}), {
597            "empty": {}
598        });
599
600        // Test array with empty object
601        assert_jm!(json!([{}]), [{}]);
602    }
603}