Skip to main content

test_better_matchers/
define.rs

1//! [`define_matcher!`], the declarative helper for the common custom-matcher
2//! case: a named predicate over a concrete target type, with a human-readable
3//! description.
4//!
5//! When a matcher needs more than a yes/no predicate (a structured diff, a
6//! borrowed-through projection, an inner matcher) it is written by hand as an
7//! `impl Matcher<T>`; see the cookbook in the `test-better` facade crate. This
8//! macro covers the case that does not need any of that.
9
10/// Defines a custom matcher from a predicate and a description.
11///
12/// This is the declarative shortcut for the most common custom matcher: one
13/// that inspects a value of a concrete type and answers yes or no, with a
14/// fixed human-readable account of what it expected. Anything richer (a diff,
15/// an inner matcher, a borrowed projection) is written by hand as an
16/// `impl Matcher<T>`.
17///
18/// # Syntax
19///
20/// (These examples name `test_better_matchers` directly because they are this
21/// crate's own doc tests; a user crate writes `use test_better::prelude::*;`
22/// and `use test_better::define_matcher;` instead.)
23///
24/// ```
25/// use test_better_matchers::define_matcher;
26///
27/// define_matcher! {
28///     /// Matches an even integer.
29///     pub fn is_even for i32 {
30///         expects: "an even integer",
31///         matches: |n| n % 2 == 0,
32///     }
33/// }
34/// ```
35///
36/// The matcher may take constructor parameters; each is in scope inside both
37/// `expects` and `matches` as a value of its declared type, so the parameter
38/// types must be [`Clone`]:
39///
40/// ```
41/// use test_better_core::TestResult;
42/// use test_better_matchers::{define_matcher, check};
43///
44/// define_matcher! {
45///     /// Matches a string that ends with `suffix`.
46///     pub fn has_suffix(suffix: &'static str) for String {
47///         expects: format!("a string ending in {suffix:?}"),
48///         matches: |actual| actual.ends_with(suffix),
49///     }
50/// }
51///
52/// fn main() -> TestResult {
53///     check!(String::from("report.csv")).satisfies(has_suffix(".csv"))?;
54///     Ok(())
55/// }
56/// ```
57///
58/// # Requirements
59///
60/// - The target type must implement [`Debug`](std::fmt::Debug): a failure
61///   reports the actual value through `{:?}`.
62/// - `expects` is any expression that converts into the matcher's description
63///   text (a string literal or a `format!` are the usual choices).
64/// - `matches` is written like a closure, `|binding| expression`, but it is
65///   not a real closure: `binding` names the borrowed actual value and the
66///   expression is its `bool` body, evaluated with the constructor parameters
67///   in scope.
68#[macro_export]
69macro_rules! define_matcher {
70    // Public form, no constructor parameters. Normalizes to the `@build` arm
71    // with an empty parameter list.
72    (
73        $(#[$meta:meta])*
74        $vis:vis fn $name:ident for $target:ty {
75            expects: $expects:expr,
76            matches: | $actual:ident | $body:expr $(,)?
77        }
78    ) => {
79        $crate::define_matcher! {
80            @build
81            $(#[$meta])*
82            $vis fn $name () for $target {
83                expects: $expects,
84                matches: | $actual | $body
85            }
86        }
87    };
88
89    // Public form, with constructor parameters.
90    (
91        $(#[$meta:meta])*
92        $vis:vis fn $name:ident ( $( $param:ident : $pty:ty ),* $(,)? ) for $target:ty {
93            expects: $expects:expr,
94            matches: | $actual:ident | $body:expr $(,)?
95        }
96    ) => {
97        $crate::define_matcher! {
98            @build
99            $(#[$meta])*
100            $vis fn $name ( $( $param : $pty ),* ) for $target {
101                expects: $expects,
102                matches: | $actual | $body
103            }
104        }
105    };
106
107    // Internal worker. The parameter list is always present (possibly empty),
108    // so there is a single shape to expand.
109    (
110        @build
111        $(#[$meta:meta])*
112        $vis:vis fn $name:ident ( $( $param:ident : $pty:ty ),* ) for $target:ty {
113            expects: $expects:expr,
114            matches: | $actual:ident | $body:expr
115        }
116    ) => {
117        $(#[$meta])*
118        $vis fn $name ( $( $param : $pty ),* ) -> impl $crate::Matcher<$target> {
119            struct __TbDefinedMatcher {
120                $( $param : $pty , )*
121            }
122
123            impl $crate::Matcher<$target> for __TbDefinedMatcher {
124                #[allow(unused_variables, clippy::clone_on_copy)]
125                fn check(&self, __tb_actual: &$target) -> $crate::MatchResult {
126                    $( let $param = ::core::clone::Clone::clone(&self.$param); )*
127                    let $actual = __tb_actual;
128                    if $body {
129                        $crate::MatchResult::pass()
130                    } else {
131                        $crate::MatchResult::fail($crate::Mismatch::new(
132                            $crate::Matcher::description(self),
133                            ::std::format!("{:?}", __tb_actual),
134                        ))
135                    }
136                }
137
138                #[allow(unused_variables, clippy::clone_on_copy)]
139                fn description(&self) -> $crate::Description {
140                    $( let $param = ::core::clone::Clone::clone(&self.$param); )*
141                    $crate::Description::text($expects)
142                }
143            }
144
145            __TbDefinedMatcher {
146                $( $param , )*
147            }
148        }
149    };
150}