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}