Skip to content

Commit 46eb42f

Browse files
committed
Simplify the Matcher trait
Now matchers need to implement a single `description` method, which will return all the necessary information to build any kind of failure message. This removes the duplication between the old `failure_message` and `negated_failure_message` and leaves the formatting logic for failure messages in one single place, the `Expectation`. I anticipate `Description` becoming more and more complex to accomodate all sorts of failures, but we should be able to make it flexible enough to accomodate all matchers.
1 parent f801c0b commit 46eb42f

File tree

7 files changed

+140
-198
lines changed

7 files changed

+140
-198
lines changed

src/lib.rs

Lines changed: 44 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,32 +21,29 @@
2121
//! [`Matcher`]: trait.Matcher.html
2222
pub mod matchers;
2323

24+
pub struct Description {
25+
pub verb: String,
26+
pub object: Option<String>,
27+
}
28+
2429
/// The contract implemented by matchers.
2530
pub trait Matcher<T> {
2631
/// Should return `true` if `actual` is a match.
2732
fn match_value(&self, actual: &T) -> bool;
2833

29-
/// Should return an appropriate message for a failure to match `actual`, i.e. a failure caused
30-
/// by `match_value` returning `false`.
31-
///
32-
/// # Example
34+
/// Should return a [`Description`] of the matcher, to be used for reporting. `actual` _should
35+
/// not_ be necessary here, but is still passed for type-checking reasons.
3336
///
34-
/// ```
35-
/// # use expect::{Matcher, matchers::equal};
36-
/// assert_eq!(equal(2).failure_message(&3), String::from("\tExpected:\n\t\t3\n\tto equal:\n\t\t2"));
37-
/// ```
38-
fn failure_message(&self, actual: &T) -> String;
39-
40-
/// Should return an appropriate message for a failure *not* to match `actual`, i.e. a failure
41-
/// caused by `match_value` returning `true`.
37+
/// [`Expectation`]: struct.Description.html
4238
///
4339
/// # Example
4440
///
4541
/// ```
46-
/// # use expect::{Matcher, matchers::equal};
47-
/// assert_eq!(equal(2).negated_failure_message(&2), String::from("\tExpected:\n\t\t2\n\tnot to equal:\n\t\t2"));
42+
/// # use expect::{Description, Matcher, matchers::EqualMatcher, matchers::equal};
43+
/// assert_eq!(equal(2).description(&2).verb, String::from("equal"));
44+
/// assert_eq!(equal(2).description(&2).object, Some(String::from("2")));
4845
/// ```
49-
fn negated_failure_message(&self, actual: &T) -> String;
46+
fn description(&self, actual: &T) -> Description;
5047
}
5148

5249
/// Creates an [`Expectation`].
@@ -63,42 +60,61 @@ pub struct Expectation<'a, T> {
6360
actual: &'a T,
6461
}
6562

66-
impl<'a, T> Expectation<'a, T> {
63+
impl<'a, T: std::fmt::Debug> Expectation<'a, T> {
6764
/// Checks the actual value agains a [`Matcher`], looking for a match.
6865
///
69-
/// If [`Matcher::match_value`] returns `false`, this method will [`panic!`] using the failure
70-
/// message returned by [`Matcher::failure_message`].
66+
/// If [`Matcher::match_value`] returns `false`, this method will [`panic!`] with a failure
67+
/// message based on the [`Matcher::description`].
7168
///
7269
/// [`Matcher`]: trait.Matcher.html
7370
/// [`Matcher::match_value`]: trait.Matcher.html#tymethod.match_value
74-
/// [`Matcher::failure_message`]: trait.Matcher.html#tymethod.failure_message
75-
/// [`Matcher::negated_failure_message`]: trait.Matcher.html#tymethod.negated_failure_message
71+
/// [`Matcher::description`]: trait.Matcher.html#tymethod.description
7672
/// [`panic!`]: https://doc.rust-lang.org/std/macro.panic.html
7773
pub fn to<M: Matcher<T>>(&self, matcher: M) {
7874
if !matcher.match_value(&self.actual) {
79-
fail_test(matcher.failure_message(&self.actual))
75+
fail_test(&self.actual, matcher.description(&self.actual))
8076
}
8177
}
8278

8379
/// Checks the actual value agains a [`Matcher`], looking for the lack of a match.
8480
///
85-
/// If [`Matcher::match_value`] returns `true`, this method will [`panic!`] using the failure
86-
/// message returned by [`Matcher::negated_failure_message`].
81+
/// If [`Matcher::match_value`] returns `true`, this method will [`panic!`] with a failure
82+
/// message based on the [`Matcher::description`].
8783
///
8884
/// [`Matcher`]: trait.Matcher.html
8985
/// [`Matcher::match_value`]: trait.Matcher.html#tymethod.match_value
90-
/// [`Matcher::failure_message`]: trait.Matcher.html#tymethod.failure_message
91-
/// [`Matcher::negated_failure_message`]: trait.Matcher.html#tymethod.negated_failure_message
86+
/// [`Matcher::description`]: trait.Matcher.html#tymethod.description
9287
/// [`panic!`]: https://doc.rust-lang.org/std/macro.panic.html
9388
pub fn not_to<M: Matcher<T>>(&self, matcher: M) {
9489
if matcher.match_value(&self.actual) {
95-
fail_test(matcher.negated_failure_message(&self.actual))
90+
fail_test_negated(&self.actual, matcher.description(&self.actual))
9691
}
9792
}
9893
}
9994

100-
fn fail_test(message: String) {
101-
panic!("Expectation failed:\n{}\n", message)
95+
fn fail_test<T: std::fmt::Debug>(actual: T, description: Description) {
96+
panic!(failure_message(actual, description, "to"))
97+
}
98+
99+
fn fail_test_negated<T: std::fmt::Debug>(actual: T, description: Description) {
100+
panic!(failure_message(actual, description, "not to"))
101+
}
102+
103+
fn failure_message<T: std::fmt::Debug>(
104+
actual: T,
105+
description: Description,
106+
before_verb: &str,
107+
) -> String {
108+
let predicate = if let Some(obj) = description.object {
109+
format!("{}:\n\t\t{}", description.verb, obj)
110+
} else {
111+
description.verb
112+
};
113+
114+
format!(
115+
"Expectation failed:\n\tExpected:\n\t\t{:?}\n\t{} {}\n",
116+
actual, before_verb, predicate
117+
)
102118
}
103119

104120
#[cfg(test)]

src/matchers.rs

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ pub mod path;
44
pub mod result;
55
pub mod string;
66

7-
use crate::Matcher;
7+
use crate::{Description, Matcher};
88

99
/// Matches if `expected` is equal to the actual value.
1010
///
@@ -23,23 +23,16 @@ pub struct EqualMatcher<T> {
2323
expected: T,
2424
}
2525

26-
impl<E: std::fmt::Debug, A: PartialEq<E> + std::fmt::Debug> Matcher<A> for EqualMatcher<E> {
26+
impl<E: std::fmt::Debug, A: PartialEq<E>> Matcher<A> for EqualMatcher<E> {
2727
fn match_value(&self, actual: &A) -> bool {
2828
actual == &self.expected
2929
}
3030

31-
fn failure_message(&self, actual: &A) -> String {
32-
format!(
33-
"\tExpected:\n\t\t{:?}\n\tto equal:\n\t\t{:?}",
34-
actual, self.expected,
35-
)
36-
}
37-
38-
fn negated_failure_message(&self, actual: &A) -> String {
39-
format!(
40-
"\tExpected:\n\t\t{:?}\n\tnot to equal:\n\t\t{:?}",
41-
actual, self.expected
42-
)
31+
fn description(&self, _actual: &A) -> Description {
32+
Description {
33+
verb: String::from("equal"),
34+
object: Some(format!("{:?}", self.expected)),
35+
}
4336
}
4437
}
4538

@@ -64,14 +57,9 @@ mod tests {
6457
}
6558

6659
#[test]
67-
fn failure_messages() {
68-
assert_eq!(
69-
equal("foo").failure_message(&"bar"),
70-
String::from("\tExpected:\n\t\t\"bar\"\n\tto equal:\n\t\t\"foo\"")
71-
);
72-
assert_eq!(
73-
equal("foo").negated_failure_message(&"foo"),
74-
String::from("\tExpected:\n\t\t\"foo\"\n\tnot to equal:\n\t\t\"foo\"")
75-
);
60+
fn should_describe_itself() {
61+
let description = equal("foo").description(&"bar");
62+
assert_eq!(description.verb, String::from("equal"));
63+
assert_eq!(description.object, Some(String::from("\"foo\"")));
7664
}
7765
}

src/matchers/collection.rs

Lines changed: 21 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::Matcher;
1+
use crate::{Description, Matcher};
22

33
use std::marker::PhantomData;
44

@@ -29,23 +29,16 @@ pub struct ContainMatcher<T> {
2929
element: T,
3030
}
3131

32-
impl<T: std::fmt::Debug, V: Collection<T> + std::fmt::Debug> Matcher<V> for ContainMatcher<T> {
32+
impl<T: std::fmt::Debug, V: Collection<T>> Matcher<V> for ContainMatcher<T> {
3333
fn match_value(&self, collection: &V) -> bool {
3434
collection.contains_element(&self.element)
3535
}
3636

37-
fn failure_message(&self, collection: &V) -> String {
38-
format!(
39-
"\tExpected:\n\t\t{:?}\n\tto contain:\n\t\t{:?}",
40-
collection, self.element
41-
)
42-
}
43-
44-
fn negated_failure_message(&self, collection: &V) -> String {
45-
format!(
46-
"\tExpected:\n\t\t{:?}\n\tnot to contain:\n\t\t{:?}",
47-
collection, self.element
48-
)
37+
fn description(&self, _: &V) -> Description {
38+
Description {
39+
verb: String::from("contain"),
40+
object: Some(format!("{:?}", self.element)),
41+
}
4942
}
5043
}
5144

@@ -77,17 +70,16 @@ pub struct BeEmptyMatcher<T> {
7770
phantom: PhantomData<T>,
7871
}
7972

80-
impl<T, V: Collection<T> + std::fmt::Debug> Matcher<V> for BeEmptyMatcher<T> {
73+
impl<T, V: Collection<T>> Matcher<V> for BeEmptyMatcher<T> {
8174
fn match_value(&self, collection: &V) -> bool {
8275
collection.empty()
8376
}
8477

85-
fn failure_message(&self, collection: &V) -> String {
86-
format!("\tExpected:\n\t\t{:?}\n\tto be empty", collection)
87-
}
88-
89-
fn negated_failure_message(&self, collection: &V) -> String {
90-
format!("\tExpected:\n\t\t{:?}\n\tnot to be empty", collection)
78+
fn description(&self, _: &V) -> Description {
79+
Description {
80+
verb: String::from("be empty"),
81+
object: None,
82+
}
9183
}
9284
}
9385

@@ -195,15 +187,10 @@ mod tests {
195187
}
196188

197189
#[test]
198-
fn contain_matcher_failure_messages() {
199-
assert_eq!(
200-
contain("foo").failure_message(&vec!["bar"]),
201-
String::from("\tExpected:\n\t\t[\"bar\"]\n\tto contain:\n\t\t\"foo\"")
202-
);
203-
assert_eq!(
204-
contain("foo").negated_failure_message(&vec!["foo"]),
205-
String::from("\tExpected:\n\t\t[\"foo\"]\n\tnot to contain:\n\t\t\"foo\"")
206-
);
190+
fn contain_matcher_should_describe_itself() {
191+
let description = contain("foo").description(&vec!["bar"]);
192+
assert_eq!(description.verb, String::from("contain"));
193+
assert_eq!(description.object, Some(String::from("\"foo\"")));
207194
}
208195

209196
#[test]
@@ -217,15 +204,10 @@ mod tests {
217204
}
218205

219206
#[test]
220-
fn be_empty_matcher_failure_messages() {
221-
assert_eq!(
222-
be_empty().failure_message(&vec!["bar"]),
223-
String::from("\tExpected:\n\t\t[\"bar\"]\n\tto be empty")
224-
);
225-
assert_eq!(
226-
be_empty().negated_failure_message(&std::vec::Vec::<i32>::new()),
227-
String::from("\tExpected:\n\t\t[]\n\tnot to be empty")
228-
);
207+
fn be_empty_matcher_should_describe_itself() {
208+
let description = be_empty().description(&vec!["foo"]);
209+
assert_eq!(description.verb, String::from("be empty"));
210+
assert_eq!(description.object, None);
229211
}
230212

231213
#[test]

src/matchers/option.rs

Lines changed: 21 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::Matcher;
1+
use crate::{Description, Matcher};
22

33
/// Matches if `actual` is an [`Option::Some`].
44
///
@@ -17,17 +17,16 @@ pub fn be_some() -> SomeMatcher {
1717

1818
pub struct SomeMatcher {}
1919

20-
impl<T: std::fmt::Debug> Matcher<Option<T>> for SomeMatcher {
20+
impl<T> Matcher<Option<T>> for SomeMatcher {
2121
fn match_value(&self, actual: &Option<T>) -> bool {
2222
actual.is_some()
2323
}
2424

25-
fn failure_message(&self, actual: &Option<T>) -> String {
26-
format!("\tExpected:\n\t\t{:?}\n\tto be a Some", actual)
27-
}
28-
29-
fn negated_failure_message(&self, actual: &Option<T>) -> String {
30-
format!("\tExpected:\n\t\t{:?}\n\tnot to be a Some", actual)
25+
fn description(&self, _: &Option<T>) -> Description {
26+
Description {
27+
verb: String::from("be a Some"),
28+
object: None,
29+
}
3130
}
3231
}
3332

@@ -48,17 +47,16 @@ pub fn be_none() -> NoneMatcher {
4847

4948
pub struct NoneMatcher {}
5049

51-
impl<T: std::fmt::Debug> Matcher<Option<T>> for NoneMatcher {
50+
impl<T> Matcher<Option<T>> for NoneMatcher {
5251
fn match_value(&self, actual: &Option<T>) -> bool {
5352
actual.is_none()
5453
}
5554

56-
fn failure_message(&self, actual: &Option<T>) -> String {
57-
format!("\tExpected:\n\t\t{:?}\n\tto be None", actual)
58-
}
59-
60-
fn negated_failure_message(&self, actual: &Option<T>) -> String {
61-
format!("\tExpected:\n\t\t{:?}\n\tnot to be None", actual)
55+
fn description(&self, _: &Option<T>) -> Description {
56+
Description {
57+
verb: String::from("be None"),
58+
object: None,
59+
}
6260
}
6361
}
6462

@@ -78,15 +76,10 @@ mod tests {
7876
}
7977

8078
#[test]
81-
fn some_matcher_failure_messages() {
82-
assert_eq!(
83-
be_some().failure_message(&None::<&str>),
84-
String::from("\tExpected:\n\t\tNone\n\tto be a Some")
85-
);
86-
assert_eq!(
87-
be_some().negated_failure_message(&Some("foo")),
88-
String::from("\tExpected:\n\t\tSome(\"foo\")\n\tnot to be a Some")
89-
);
79+
fn some_matcher_should_describe_itself() {
80+
let description = be_some().description(&Some("bar"));
81+
assert_eq!(description.verb, String::from("be a Some"));
82+
assert_eq!(description.object, None);
9083
}
9184

9285
#[test]
@@ -100,14 +93,9 @@ mod tests {
10093
}
10194

10295
#[test]
103-
fn none_matcher_failure_messages() {
104-
assert_eq!(
105-
be_none().failure_message(&Some("foo")),
106-
String::from("\tExpected:\n\t\tSome(\"foo\")\n\tto be None")
107-
);
108-
assert_eq!(
109-
be_none().negated_failure_message(&None::<&str>),
110-
String::from("\tExpected:\n\t\tNone\n\tnot to be None")
111-
);
96+
fn none_matcher_should_describe_itself() {
97+
let description = be_none().description(&None::<&str>);
98+
assert_eq!(description.verb, String::from("be None"));
99+
assert_eq!(description.object, None);
112100
}
113101
}

0 commit comments

Comments
 (0)