From eb261ba0c17006e0bba84ac52c89f28ea9f110a8 Mon Sep 17 00:00:00 2001 From: haraldmaida Date: Sun, 29 Mar 2026 19:13:15 +0200 Subject: [PATCH 01/24] feat: introduce `Spec::extracting_ref` and `And` trait for extracting multiple fields on same subject for assertion --- src/assertions.rs | 2 +- src/extracting/mod.rs | 1205 +++++++++++++++++++++++++++++++++++++++ src/extracting/tests.rs | 871 ++++++++++++++++++++++++++++ src/lib.rs | 1 + src/option/mod.rs | 6 +- src/prelude.rs | 2 +- src/result/mod.rs | 5 +- src/spec/mod.rs | 42 +- 8 files changed, 2117 insertions(+), 17 deletions(-) create mode 100644 src/extracting/mod.rs create mode 100644 src/extracting/tests.rs diff --git a/src/assertions.rs b/src/assertions.rs index 59626dd..ff9d629 100644 --- a/src/assertions.rs +++ b/src/assertions.rs @@ -947,7 +947,7 @@ pub trait AssertDecimalNumber { /// # } /// ``` /// - /// Note: `rust_decimal::Decimal` is fixed precision decimal number. The + /// Note: `rust_decimal::Decimal` is a fixed precision decimal number. The /// actual precision is always 29. #[track_caller] fn has_precision_of(self, expected_precision: u64) -> Self; diff --git a/src/extracting/mod.rs b/src/extracting/mod.rs new file mode 100644 index 0000000..3917d44 --- /dev/null +++ b/src/extracting/mod.rs @@ -0,0 +1,1205 @@ +use crate::assertions::{ + AssertBoolean, AssertChar, AssertDebugString, AssertDecimalNumber, AssertDisplayString, + AssertEmptiness, AssertEquality, AssertErrorHasSource, AssertHasCharCount, + AssertHasDebugString, AssertHasDisplayString, AssertHasError, AssertHasErrorMessage, + AssertHasLength, AssertHasValue, AssertInRange, AssertInfinity, AssertIsSorted, + AssertIteratorContains, AssertIteratorContainsInAnyOrder, AssertIteratorContainsInOrder, + AssertNotANumber, AssertNumericIdentity, AssertOption, AssertOptionValue, AssertOrder, + AssertResult, AssertResultValue, AssertSameAs, AssertSignum, AssertStringContainsAnyOf, + AssertStringPattern, +}; +use crate::expectations::{ + error_has_source, error_has_source_message, has_at_least_char_count, has_at_least_length, + has_at_most_char_count, has_at_most_length, has_char_count, has_char_count_greater_than, + has_char_count_in_range, has_char_count_less_than, has_debug_string, has_display_string, + has_error, has_length, has_length_greater_than, has_length_in_range, has_length_less_than, + has_precision_of, has_scale_of, has_value, is_a_number, is_after, is_alphabetic, + is_alphanumeric, is_ascii, is_at_least, is_at_most, is_before, is_between, is_control_char, + is_digit, is_empty, is_equal_to, is_err, is_false, is_finite, is_greater_than, is_in_range, + is_infinite, is_integer, is_less_than, is_lower_case, is_negative, is_none, is_ok, is_one, + is_positive, is_same_as, is_some, is_true, is_upper_case, is_whitespace, is_zero, + iterator_contains, iterator_contains_all_in_order, iterator_contains_all_of, + iterator_contains_any_of, iterator_contains_exactly, iterator_contains_exactly_in_any_order, + iterator_contains_only, iterator_contains_only_once, iterator_contains_sequence, + iterator_ends_with, iterator_starts_with, not, string_contains, string_contains_any_of, + string_ends_with, string_starts_with, +}; +use crate::properties::{ + AdditiveIdentityProperty, CharCountProperty, DecimalProperties, DefinedOrderProperty, + InfinityProperty, IsEmptyProperty, IsNanProperty, LengthProperty, + MultiplicativeIdentityProperty, SignumProperty, +}; +use crate::spec::{ + And, AssertFailure, DiffFormat, DoFail, Expectation, Expression, GetFailures, SoftPanic, +}; +use crate::std::borrow::{Cow, ToOwned}; +use crate::std::error::Error; +use crate::std::fmt::{Debug, Display}; +use crate::std::format; +use crate::std::ops::RangeBounds; +use crate::std::string::{String, ToString}; +use crate::std::vec::Vec; + +pub struct DerivedSpec<'a, O, S> { + original: O, + subject: S, + expression: Expression<'a>, + diff_format: DiffFormat, +} + +impl GetFailures for DerivedSpec<'_, O, S> +where + O: GetFailures, +{ + fn has_failures(&self) -> bool { + self.original.has_failures() + } + + fn failures(&self) -> Vec { + self.original.failures() + } + + fn display_failures(&self) -> Vec { + self.original.display_failures() + } +} + +impl DerivedSpec<'_, O, S> { + /// Returns the expression (or subject name) if one has been set. + pub fn expression(&self) -> &Expression<'_> { + &self.expression + } + + /// Returns the diff format used with this assertion. + pub const fn diff_format(&self) -> &DiffFormat { + &self.diff_format + } +} + +impl<'a, O, S> DerivedSpec<'a, O, S> { + #[must_use = "a derived spec does nothing unless an assertion method is called"] + pub(crate) fn new( + original: O, + derived_subject: S, + expression: Expression<'a>, + diff_format: DiffFormat, + ) -> Self { + Self { + original, + subject: derived_subject, + expression, + diff_format, + } + } + + /// Sets the subject name or expression for this assertion. + #[must_use = "a derived spec does nothing unless an assertion method is called"] + pub fn named(mut self, subject_name: impl Into>) -> Self { + self.expression = Expression(subject_name.into()); + self + } + + /// Sets the diff format used to highlight differences between the actual + /// value and the expected value. + /// + /// Note: This method must be called before an assertion method is called to + /// affect the failure message of the assertion as failure messages are + /// formatted immediately when an assertion is executed. + #[must_use = "a spec does nothing unless an assertion method is called"] + pub const fn with_diff_format(mut self, diff_format: DiffFormat) -> Self { + self.diff_format = diff_format; + self + } + + /// Sets the diff format used to highlight differences between the actual + /// value and the expected value according to the configured mode. + /// + /// The mode is configured via environment variables as described in the + /// module [colored]. + #[cfg(feature = "colored")] + #[cfg_attr(docsrs, doc(cfg(feature = "colored")))] + #[must_use = "a spec does nothing unless an assertion method is called"] + pub fn with_configured_diff_format(self) -> Self { + use crate::colored::configured_diff_format; + #[cfg(not(feature = "std"))] + { + self.with_diff_format(configured_diff_format()) + } + #[cfg(feature = "std")] + { + use crate::std::sync::OnceLock; + static DIFF_FORMAT: OnceLock = OnceLock::new(); + let diff_format = DIFF_FORMAT.get_or_init(configured_diff_format); + self.with_diff_format(diff_format.clone()) + } + } +} + +impl DoFail for DerivedSpec<'_, O, S> +where + O: DoFail, +{ + fn do_fail_with(&mut self, failures: impl IntoIterator) { + self.original.do_fail_with(failures); + } + + fn do_fail_with_message(&mut self, message: impl Into) { + self.original.do_fail_with_message(message); + } +} + +impl SoftPanic for DerivedSpec<'_, O, S> +where + O: SoftPanic, +{ + fn soft_panic(&self) { + self.original.soft_panic(); + } +} + +impl And for DerivedSpec<'_, O, S> { + type Target = O; + + fn and(self) -> Self::Target { + self.original + } +} + +impl<'a, O, S> DerivedSpec<'a, O, S> { + #[must_use = "a derived spec does nothing unless an assertion method is called"] + pub fn extracting_ref(self, extract: F) -> DerivedSpec<'a, Self, U> + where + F: FnOnce(&S) -> &B, + B: ToOwned + ?Sized, + { + let extracted = extract(&self.subject).to_owned(); + let expression = Expression::default(); + let diff_format = self.diff_format.clone(); + DerivedSpec { + original: self, + subject: extracted, + expression, + diff_format, + } + } + + #[must_use = "a derived spec does nothing unless an assertion method is called"] + pub fn extracting(self, extract: F) -> DerivedSpec<'a, O, U> + where + F: FnOnce(S) -> U, + { + let extracted = extract(self.subject); + let diff_format = self.diff_format.clone(); + DerivedSpec { + original: self.original, + subject: extracted, + expression: Expression::default(), + diff_format, + } + } + + #[must_use = "a derived spec does nothing unless an assertion method is called"] + pub fn mapping(self, map: F) -> DerivedSpec<'a, O, U> + where + F: FnOnce(S) -> U, + { + let mapped = map(self.subject); + DerivedSpec { + original: self.original, + subject: mapped, + expression: self.expression, + diff_format: self.diff_format, + } + } +} + +impl DerivedSpec<'_, O, S> +where + O: DoFail, +{ + #[allow(clippy::needless_pass_by_value, clippy::return_self_not_must_use)] + #[track_caller] + pub fn expecting(mut self, mut expectation: impl Expectation) -> Self { + if !expectation.test(&self.subject) { + let message = + expectation.message(&self.expression, &self.subject, false, &self.diff_format); + self.do_fail_with_message(message); + } + self + } +} + +impl AssertEquality for DerivedSpec<'_, O, S> +where + S: PartialEq + Debug, + E: Debug, + O: DoFail, +{ + fn is_equal_to(self, expected: E) -> Self { + self.expecting(is_equal_to(expected)) + } + + fn is_not_equal_to(self, expected: E) -> Self { + self.expecting(not(is_equal_to(expected))) + } +} + +impl AssertSameAs for DerivedSpec<'_, O, S> +where + S: PartialEq + Debug, + O: DoFail, +{ + fn is_same_as(self, expected: S) -> Self { + self.expecting(is_same_as(expected)) + } + + fn is_not_same_as(self, expected: S) -> Self { + self.expecting(not(is_same_as(expected))) + } +} + +#[cfg(feature = "float-cmp")] +mod float_cmp { + use super::DerivedSpec; + use crate::assertions::{AssertIsCloseToWithDefaultMargin, AssertIsCloseToWithinMargin}; + use crate::expectations::{is_close_to, not}; + use crate::spec::DoFail; + use float_cmp::{F32Margin, F64Margin}; + + impl AssertIsCloseToWithinMargin for DerivedSpec<'_, O, f32> + where + O: DoFail, + { + fn is_close_to_with_margin(self, expected: f32, margin: impl Into) -> Self { + self.expecting(is_close_to(expected).within_margin(margin)) + } + + fn is_not_close_to_with_margin(self, expected: f32, margin: impl Into) -> Self { + self.expecting(not(is_close_to(expected).within_margin(margin))) + } + } + + impl AssertIsCloseToWithDefaultMargin for DerivedSpec<'_, O, f32> + where + O: DoFail, + { + fn is_close_to(self, expected: f32) -> Self { + self.expecting(is_close_to(expected).within_margin((4. * f32::EPSILON, 4))) + } + + fn is_not_close_to(self, expected: f32) -> Self { + self.expecting(not( + is_close_to(expected).within_margin((4. * f32::EPSILON, 4)) + )) + } + } + + impl AssertIsCloseToWithinMargin for DerivedSpec<'_, O, f64> + where + O: DoFail, + { + fn is_close_to_with_margin(self, expected: f64, margin: impl Into) -> Self { + self.expecting(is_close_to(expected).within_margin(margin)) + } + + fn is_not_close_to_with_margin(self, expected: f64, margin: impl Into) -> Self { + self.expecting(not(is_close_to(expected).within_margin(margin))) + } + } + + impl AssertIsCloseToWithDefaultMargin for DerivedSpec<'_, O, f64> + where + O: DoFail, + { + fn is_close_to(self, expected: f64) -> Self { + self.expecting(is_close_to(expected).within_margin((4. * f64::EPSILON, 4))) + } + + fn is_not_close_to(self, expected: f64) -> Self { + self.expecting(not( + is_close_to(expected).within_margin((4. * f64::EPSILON, 4)) + )) + } + } +} + +impl AssertOrder for DerivedSpec<'_, O, S> +where + S: PartialOrd + Debug, + E: Debug, + O: DoFail, +{ + fn is_less_than(self, expected: E) -> Self { + self.expecting(is_less_than(expected)) + } + + fn is_greater_than(self, expected: E) -> Self { + self.expecting(is_greater_than(expected)) + } + + fn is_at_most(self, expected: E) -> Self { + self.expecting(is_at_most(expected)) + } + + fn is_at_least(self, expected: E) -> Self { + self.expecting(is_at_least(expected)) + } + + fn is_before(self, expected: E) -> Self { + self.expecting(is_before(expected)) + } + + fn is_after(self, expected: E) -> Self { + self.expecting(is_after(expected)) + } + + fn is_between(self, min: E, max: E) -> Self { + self.expecting(is_between(min, max)) + } +} + +impl AssertInRange for DerivedSpec<'_, O, S> +where + S: PartialOrd + Debug, + E: PartialOrd + Debug, + O: DoFail, +{ + fn is_in_range(self, range: R) -> Self + where + R: RangeBounds + Debug, + { + self.expecting(is_in_range(range)) + } + + fn is_not_in_range(self, range: R) -> Self + where + R: RangeBounds + Debug, + { + self.expecting(not(is_in_range(range))) + } +} + +impl AssertNumericIdentity for DerivedSpec<'_, O, S> +where + S: AdditiveIdentityProperty + MultiplicativeIdentityProperty + PartialEq + Debug, + O: DoFail, +{ + fn is_zero(self) -> Self { + self.expecting(is_zero()) + } + + fn is_one(self) -> Self { + self.expecting(is_one()) + } +} + +impl AssertSignum for DerivedSpec<'_, O, S> +where + S: SignumProperty + Debug, + O: DoFail, +{ + fn is_negative(self) -> Self { + self.expecting(is_negative()) + } + + fn is_not_negative(self) -> Self { + self.expecting(not(is_negative())) + } + + fn is_positive(self) -> Self { + self.expecting(is_positive()) + } + + fn is_not_positive(self) -> Self { + self.expecting(not(is_positive())) + } +} + +impl AssertInfinity for DerivedSpec<'_, O, S> +where + S: InfinityProperty + Debug, + O: DoFail, +{ + fn is_infinite(self) -> Self { + self.expecting(is_infinite()) + } + + fn is_finite(self) -> Self { + self.expecting(is_finite()) + } +} + +impl AssertNotANumber for DerivedSpec<'_, O, S> +where + S: IsNanProperty + Debug, + O: DoFail, +{ + fn is_not_a_number(self) -> Self { + self.expecting(not(is_a_number())) + } + + fn is_a_number(self) -> Self { + self.expecting(is_a_number()) + } +} + +impl AssertDecimalNumber for DerivedSpec<'_, O, S> +where + S: DecimalProperties + Debug, + O: DoFail, +{ + fn has_scale_of(self, expected_scale: i64) -> Self { + self.expecting(has_scale_of(expected_scale)) + } + + fn has_precision_of(self, expected_precision: u64) -> Self { + self.expecting(has_precision_of(expected_precision)) + } + + fn is_integer(self) -> Self { + self.expecting(is_integer()) + } +} + +impl AssertBoolean for DerivedSpec<'_, O, bool> +where + O: DoFail, +{ + fn is_true(self) -> Self { + self.expecting(is_true()) + } + + fn is_false(self) -> Self { + self.expecting(is_false()) + } +} + +impl AssertChar for DerivedSpec<'_, O, char> +where + O: DoFail, +{ + fn is_lowercase(self) -> Self { + self.expecting(is_lower_case()) + } + + fn is_uppercase(self) -> Self { + self.expecting(is_upper_case()) + } + + fn is_ascii(self) -> Self { + self.expecting(is_ascii()) + } + + fn is_alphabetic(self) -> Self { + self.expecting(is_alphabetic()) + } + + fn is_alphanumeric(self) -> Self { + self.expecting(is_alphanumeric()) + } + + fn is_control_char(self) -> Self { + self.expecting(is_control_char()) + } + + fn is_digit(self, radix: u32) -> Self { + self.expecting(is_digit(radix)) + } + + fn is_whitespace(self) -> Self { + self.expecting(is_whitespace()) + } +} + +impl AssertChar for DerivedSpec<'_, O, &char> +where + O: DoFail, +{ + fn is_lowercase(self) -> Self { + self.expecting(is_lower_case()) + } + + fn is_uppercase(self) -> Self { + self.expecting(is_upper_case()) + } + + fn is_ascii(self) -> Self { + self.expecting(is_ascii()) + } + + fn is_alphabetic(self) -> Self { + self.expecting(is_alphabetic()) + } + + fn is_alphanumeric(self) -> Self { + self.expecting(is_alphanumeric()) + } + + fn is_control_char(self) -> Self { + self.expecting(is_control_char()) + } + + fn is_digit(self, radix: u32) -> Self { + self.expecting(is_digit(radix)) + } + + fn is_whitespace(self) -> Self { + self.expecting(is_whitespace()) + } +} + +impl AssertEmptiness for DerivedSpec<'_, O, S> +where + S: IsEmptyProperty + Debug, + O: DoFail, +{ + fn is_empty(self) -> Self { + self.expecting(is_empty()) + } + + fn is_not_empty(self) -> Self { + self.expecting(not(is_empty())) + } +} + +impl AssertHasLength for DerivedSpec<'_, O, S> +where + S: LengthProperty + Debug, + O: DoFail, +{ + fn has_length(self, expected_length: usize) -> Self { + self.expecting(has_length(expected_length)) + } + + fn has_length_in_range(self, expected_range: R) -> Self + where + R: RangeBounds + Debug, + { + self.expecting(has_length_in_range(expected_range)) + } + + fn has_length_less_than(self, expected_length: usize) -> Self { + self.expecting(has_length_less_than(expected_length)) + } + + fn has_length_greater_than(self, expected_length: usize) -> Self { + self.expecting(has_length_greater_than(expected_length)) + } + + fn has_at_most_length(self, expected_length: usize) -> Self { + self.expecting(has_at_most_length(expected_length)) + } + + fn has_at_least_length(self, expected_length: usize) -> Self { + self.expecting(has_at_least_length(expected_length)) + } +} + +impl AssertHasCharCount for DerivedSpec<'_, O, S> +where + S: CharCountProperty + Debug, + O: DoFail, +{ + fn has_char_count(self, expected_char_count: usize) -> Self { + self.expecting(has_char_count(expected_char_count)) + } + + fn has_char_count_in_range(self, expected_range: U) -> Self + where + U: RangeBounds + Debug, + { + self.expecting(has_char_count_in_range(expected_range)) + } + + fn has_char_count_less_than(self, expected_char_count: usize) -> Self { + self.expecting(has_char_count_less_than(expected_char_count)) + } + + fn has_char_count_greater_than(self, expected_char_count: usize) -> Self { + self.expecting(has_char_count_greater_than(expected_char_count)) + } + + fn has_at_most_char_count(self, expected_char_count: usize) -> Self { + self.expecting(has_at_most_char_count(expected_char_count)) + } + + fn has_at_least_char_count(self, expected_char_count: usize) -> Self { + self.expecting(has_at_least_char_count(expected_char_count)) + } +} + +impl AssertOption for DerivedSpec<'_, O, Option> +where + S: Debug, + O: DoFail, +{ + fn is_some(self) -> Self { + self.expecting(is_some()) + } + + fn is_none(self) -> Self { + self.expecting(is_none()) + } +} + +impl<'a, O, T> AssertOptionValue for DerivedSpec<'a, O, Option> +where + O: DoFail, +{ + type Some = DerivedSpec<'a, O, T>; + + fn some(self) -> Self::Some { + self.mapping(|subject| match subject { + None => { + panic!("expected the subject to be `Some(_)`, but was `None`") + }, + Some(value) => value, + }) + } +} + +impl<'a, O, T> AssertOptionValue for DerivedSpec<'a, O, &'a Option> +where + T: 'a, + O: DoFail, +{ + type Some = DerivedSpec<'a, O, &'a T>; + + fn some(self) -> Self::Some { + self.mapping(|subject| match subject { + None => { + panic!("expected the subject to be `Some(_)`, but was `None`") + }, + Some(value) => value, + }) + } +} + +impl AssertHasValue for DerivedSpec<'_, O, Option> +where + T: PartialEq + Debug, + E: Debug, + O: DoFail, +{ + fn has_value(self, expected: E) -> Self { + self.expecting(has_value(expected)) + } +} + +impl AssertHasValue for DerivedSpec<'_, O, &Option> +where + T: PartialEq + Debug, + E: Debug, + O: DoFail, +{ + fn has_value(self, expected: E) -> Self { + self.expecting(has_value(expected)) + } +} + +impl AssertResult for DerivedSpec<'_, O, Result> +where + T: Debug, + E: Debug, + O: DoFail, +{ + fn is_ok(self) -> Self { + self.expecting(is_ok()) + } + + fn is_err(self) -> Self { + self.expecting(is_err()) + } +} + +impl AssertResult for DerivedSpec<'_, O, &Result> +where + T: Debug, + E: Debug, + O: DoFail, +{ + fn is_ok(self) -> Self { + self.expecting(is_ok()) + } + + fn is_err(self) -> Self { + self.expecting(is_err()) + } +} + +impl<'a, O, T, E> AssertResultValue for DerivedSpec<'a, O, Result> +where + T: Debug, + E: Debug, + O: DoFail, +{ + type Ok = DerivedSpec<'a, O, T>; + type Err = DerivedSpec<'a, O, E>; + + fn ok(self) -> Self::Ok { + self.mapping(|subject| match subject { + Ok(value) => value, + Err(error) => { + panic!("expected the subject to be `Ok(_)`, but was `Err({error:?})`") + }, + }) + } + + fn err(self) -> Self::Err { + self.mapping(|subject| match subject { + Ok(value) => { + panic!("expected the subject to be `Err(_)`, but was `Ok({value:?})`") + }, + Err(error) => error, + }) + } +} + +impl<'a, O, T, E> AssertResultValue for DerivedSpec<'a, O, &'a Result> +where + T: Debug, + E: Debug, + O: DoFail, +{ + type Ok = DerivedSpec<'a, O, &'a T>; + type Err = DerivedSpec<'a, O, &'a E>; + + fn ok(self) -> Self::Ok { + self.mapping(|subject| match subject { + Ok(value) => value, + Err(error) => { + panic!("expected the subject to be `Ok(_)`, but was `Err({error:?})`") + }, + }) + } + + fn err(self) -> Self::Err { + self.mapping(|subject| match subject { + Ok(value) => { + panic!("expected the subject to be `Err(_)`, but was `Ok({value:?})`") + }, + Err(error) => error, + }) + } +} + +impl AssertHasValue for DerivedSpec<'_, O, Result> +where + T: PartialEq + Debug, + E: Debug, + X: Debug, + O: DoFail, +{ + fn has_value(self, expected: X) -> Self { + self.expecting(has_value(expected)) + } +} + +impl AssertHasValue for DerivedSpec<'_, O, &Result> +where + T: PartialEq + Debug, + E: Debug, + X: Debug, + O: DoFail, +{ + fn has_value(self, expected: X) -> Self { + self.expecting(has_value(expected)) + } +} + +impl AssertHasError for DerivedSpec<'_, O, Result> +where + T: Debug, + E: PartialEq + Debug, + X: Debug, + O: DoFail, +{ + fn has_error(self, expected: X) -> Self { + self.expecting(has_error(expected)) + } +} + +impl AssertHasError for DerivedSpec<'_, O, &Result> +where + T: Debug, + E: PartialEq + Debug, + X: Debug, + O: DoFail, +{ + fn has_error(self, expected: X) -> Self { + self.expecting(has_error(expected)) + } +} + +impl<'a, O, T, E, X> AssertHasErrorMessage for DerivedSpec<'a, O, Result> +where + T: Debug, + E: Display, + X: Debug, + String: PartialEq, + O: DoFail, +{ + type ErrorMessage = DerivedSpec<'a, O, String>; + + fn has_error_message(self, expected: X) -> Self::ErrorMessage { + self.mapping(|result| match result { + Ok(value) => panic!("expected the subject to be `Err(_)` with message {expected:?}, but was `Ok({value:?})`"), + Err(error) => error.to_string(), + }).expecting(is_equal_to(expected)) + } +} + +impl<'a, O, T, E, X> AssertHasErrorMessage for DerivedSpec<'a, O, &Result> +where + T: Debug, + E: Display, + X: Debug, + String: PartialEq, + O: DoFail, +{ + type ErrorMessage = DerivedSpec<'a, O, String>; + + fn has_error_message(self, expected: X) -> Self::ErrorMessage { + self.mapping(|result| match result { + Ok(value) => panic!("expected the subject to be `Err(_)` with message {expected:?}, but was `Ok({value:?})`"), + Err(error) => error.to_string(), + }).expecting(is_equal_to(expected)) + } +} + +impl<'a, O, S> AssertErrorHasSource for DerivedSpec<'a, O, S> +where + S: Error, + O: DoFail, +{ + type SourceMessage = DerivedSpec<'a, O, Option>; + + fn has_no_source(self) -> Self { + self.expecting(not(error_has_source())) + } + + fn has_source(self) -> Self { + self.expecting(error_has_source()) + } + + fn has_source_message(self, expected_source_message: impl Into) -> Self::SourceMessage { + let expected_source_message = expected_source_message.into(); + self.expecting(error_has_source_message(expected_source_message)) + .mapping(|err| err.source().map(ToString::to_string)) + } +} + +impl AssertHasDebugString for DerivedSpec<'_, O, S> +where + S: Debug, + E: AsRef, + O: DoFail, +{ + fn has_debug_string(self, expected: E) -> Self { + self.expecting(has_debug_string(expected)) + } + + fn does_not_have_debug_string(self, expected: E) -> Self { + self.expecting(not(has_debug_string(expected))) + } +} + +impl<'a, O, S> AssertDebugString for DerivedSpec<'a, O, S> +where + S: Debug, + O: DoFail, +{ + type DebugString = DerivedSpec<'a, O, String>; + + fn debug_string(self) -> Self::DebugString { + let expression_debug_string = format!("{}'s debug string", self.expression); + self.mapping(|subject| format!("{subject:?}")) + .named(expression_debug_string) + } +} + +impl AssertHasDisplayString for DerivedSpec<'_, O, S> +where + S: Display, + E: AsRef, + O: DoFail, +{ + fn has_display_string(self, expected: E) -> Self { + self.expecting(has_display_string(expected)) + } + + fn does_not_have_display_string(self, expected: E) -> Self { + self.expecting(not(has_display_string(expected))) + } +} + +impl<'a, O, S> AssertDisplayString for DerivedSpec<'a, O, S> +where + S: Display, + O: DoFail, +{ + type DisplayString = DerivedSpec<'a, O, String>; + + fn display_string(self) -> Self::DisplayString { + let expression_display_string = format!("{}'s display string", self.expression); + self.mapping(|subject| subject.to_string()) + .named(expression_display_string) + } +} + +impl<'a, O, S> AssertStringPattern<&'a str> for DerivedSpec<'a, O, S> +where + S: 'a + AsRef + Debug, + O: DoFail, +{ + fn contains(self, pattern: &'a str) -> Self { + self.expecting(string_contains(pattern)) + } + + fn does_not_contain(self, pattern: &'a str) -> Self { + self.expecting(not(string_contains(pattern))) + } + + fn starts_with(self, pattern: &'a str) -> Self { + self.expecting(string_starts_with(pattern)) + } + + fn does_not_start_with(self, pattern: &'a str) -> Self { + self.expecting(not(string_starts_with(pattern))) + } + + fn ends_with(self, pattern: &'a str) -> Self { + self.expecting(string_ends_with(pattern)) + } + + fn does_not_end_with(self, pattern: &'a str) -> Self { + self.expecting(not(string_ends_with(pattern))) + } +} + +impl<'a, O, S> AssertStringPattern for DerivedSpec<'a, O, S> +where + S: 'a + AsRef + Debug, + O: DoFail, +{ + fn contains(self, pattern: String) -> Self { + self.expecting(string_contains(pattern)) + } + + fn does_not_contain(self, pattern: String) -> Self { + self.expecting(not(string_contains(pattern))) + } + + fn starts_with(self, pattern: String) -> Self { + self.expecting(string_starts_with(pattern)) + } + + fn does_not_start_with(self, pattern: String) -> Self { + self.expecting(not(string_starts_with(pattern))) + } + + fn ends_with(self, pattern: String) -> Self { + self.expecting(string_ends_with(pattern)) + } + + fn does_not_end_with(self, pattern: String) -> Self { + self.expecting(not(string_ends_with(pattern))) + } +} + +impl<'a, O, S> AssertStringPattern for DerivedSpec<'a, O, S> +where + S: 'a + AsRef + Debug, + O: DoFail, +{ + fn contains(self, pattern: char) -> Self { + self.expecting(string_contains(pattern)) + } + + fn does_not_contain(self, pattern: char) -> Self { + self.expecting(not(string_contains(pattern))) + } + + fn starts_with(self, pattern: char) -> Self { + self.expecting(string_starts_with(pattern)) + } + + fn does_not_start_with(self, pattern: char) -> Self { + self.expecting(not(string_starts_with(pattern))) + } + + fn ends_with(self, pattern: char) -> Self { + self.expecting(string_ends_with(pattern)) + } + + fn does_not_end_with(self, pattern: char) -> Self { + self.expecting(not(string_ends_with(pattern))) + } +} + +impl<'a, O, S> AssertStringContainsAnyOf<&'a [char]> for DerivedSpec<'a, O, S> +where + S: 'a + AsRef + Debug, + O: DoFail, +{ + fn contains_any_of(self, expected: &'a [char]) -> Self { + self.expecting(string_contains_any_of(expected)) + } + + fn does_not_contain_any_of(self, expected: &'a [char]) -> Self { + self.expecting(not(string_contains_any_of(expected))) + } +} + +impl<'a, O, S, const N: usize> AssertStringContainsAnyOf<[char; N]> for DerivedSpec<'a, O, S> +where + S: 'a + AsRef + Debug, + O: DoFail, +{ + fn contains_any_of(self, expected: [char; N]) -> Self { + self.expecting(string_contains_any_of(expected)) + } + + fn does_not_contain_any_of(self, expected: [char; N]) -> Self { + self.expecting(not(string_contains_any_of(expected))) + } +} + +impl<'a, O, S, const N: usize> AssertStringContainsAnyOf<&'a [char; N]> for DerivedSpec<'a, O, S> +where + S: 'a + AsRef + Debug, + O: DoFail, +{ + fn contains_any_of(self, expected: &'a [char; N]) -> Self { + self.expecting(string_contains_any_of(expected)) + } + + fn does_not_contain_any_of(self, expected: &'a [char; N]) -> Self { + self.expecting(not(string_contains_any_of(expected))) + } +} + +#[cfg(feature = "regex")] +mod regex { + use crate::assertions::AssertStringMatches; + use crate::expectations::{not, string_matches}; + use crate::extracting::DerivedSpec; + use crate::spec::DoFail; + use crate::std::fmt::Debug; + + impl AssertStringMatches for DerivedSpec<'_, O, S> + where + S: AsRef + Debug, + O: DoFail, + { + fn matches(self, regex_pattern: &str) -> Self { + self.expecting(string_matches(regex_pattern)) + } + + fn does_not_match(self, regex_pattern: &str) -> Self { + self.expecting(not(string_matches(regex_pattern))) + } + } +} + +impl<'a, O, S, T, E> AssertIteratorContains for DerivedSpec<'a, O, S> +where + S: IntoIterator, + T: PartialEq + Debug, + E: Debug, + O: DoFail, +{ + type Sequence = DerivedSpec<'a, O, Vec>; + + fn contains(self, element: E) -> Self::Sequence { + self.mapping(Vec::from_iter) + .expecting(iterator_contains(element)) + } + + fn does_not_contain(self, element: E) -> Self::Sequence { + self.mapping(Vec::from_iter) + .expecting(not(iterator_contains(element))) + } +} + +impl<'a, O, S, T, E> AssertIteratorContainsInAnyOrder for DerivedSpec<'a, O, S> +where + S: IntoIterator, + T: PartialEq<::Item> + Debug, + E: IntoIterator, + ::Item: Debug, + O: DoFail, +{ + type Sequence = DerivedSpec<'a, O, Vec>; + + fn contains_exactly_in_any_order(self, expected: E) -> Self::Sequence { + self.mapping(Vec::from_iter) + .expecting(iterator_contains_exactly_in_any_order(expected)) + } + + fn contains_any_of(self, expected: E) -> Self::Sequence { + self.mapping(Vec::from_iter) + .expecting(iterator_contains_any_of(expected)) + } + + fn does_not_contain_any_of(self, expected: E) -> Self::Sequence { + self.mapping(Vec::from_iter) + .expecting(not(iterator_contains_any_of(expected))) + } + + fn contains_all_of(self, expected: E) -> Self::Sequence { + self.mapping(Vec::from_iter) + .expecting(iterator_contains_all_of(expected)) + } + + fn contains_only(self, expected: E) -> Self::Sequence { + self.mapping(Vec::from_iter) + .expecting(iterator_contains_only(expected)) + } + + fn contains_only_once(self, expected: E) -> Self::Sequence { + self.mapping(Vec::from_iter) + .expecting(iterator_contains_only_once(expected)) + } +} + +impl<'a, O, S, T, E> AssertIteratorContainsInOrder for DerivedSpec<'a, O, S> +where + S: IntoIterator, + ::IntoIter: DefinedOrderProperty, + E: IntoIterator, + ::IntoIter: DefinedOrderProperty, + ::Item: Debug, + T: PartialEq<::Item> + Debug, + O: DoFail, +{ + type Sequence = DerivedSpec<'a, O, Vec>; + + fn contains_exactly(self, expected: E) -> Self::Sequence { + self.mapping(Vec::from_iter) + .expecting(iterator_contains_exactly(expected)) + } + + fn contains_sequence(self, expected: E) -> Self::Sequence { + self.mapping(Vec::from_iter) + .expecting(iterator_contains_sequence(expected)) + } + + fn contains_all_in_order(self, expected: E) -> Self::Sequence { + self.mapping(Vec::from_iter) + .expecting(iterator_contains_all_in_order(expected)) + } + + fn starts_with(self, expected: E) -> Self::Sequence { + self.mapping(Vec::from_iter) + .expecting(iterator_starts_with(expected)) + } + + fn ends_with(self, expected: E) -> Self::Sequence { + self.mapping(Vec::from_iter) + .expecting(iterator_ends_with(expected)) + } +} + +#[cfg(test)] +mod tests; diff --git a/src/extracting/tests.rs b/src/extracting/tests.rs new file mode 100644 index 0000000..6043468 --- /dev/null +++ b/src/extracting/tests.rs @@ -0,0 +1,871 @@ +use crate::prelude::*; +use crate::std::string::{String, ToString}; +use crate::std::vec; +use crate::std::vec::Vec; +#[cfg(feature = "bigdecimal")] +use bigdecimal::BigDecimal; +#[cfg(feature = "float-cmp")] +use time::macros::datetime; +#[cfg(feature = "float-cmp")] +use time::OffsetDateTime; + +#[cfg(feature = "float-cmp")] +#[derive(Debug, Clone, PartialEq)] +struct Item { + name: String, + price: f32, + quantity: u32, +} + +#[cfg(feature = "float-cmp")] +struct Order { + id: String, + purchased_at: OffsetDateTime, + items: Vec, + vat: f32, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum Gender { + Male, + Female, + NonBinary, + PreferNotToSay, +} + +struct Person { + name: String, + age: u8, + gender: Gender, +} + +impl Person { + fn name(&self) -> &str { + &self.name + } +} + +#[test] +fn mapping_person_name_starts_with_alex() { + let person = Person { + name: "Alexander".to_string(), + age: 31, + gender: Gender::Male, + }; + + assert_that(person).mapping(|p| p.name).starts_with("Alex"); +} + +#[test] +fn extracting_person_name_contains_i() { + let person = Person { + name: "Silvia".to_string(), + age: 27, + gender: Gender::Female, + }; + + assert_that(person).extracting(|p| p.name).contains('i'); +} + +#[test] +fn extracting_ref_person_name_via_accessor_contains_via() { + let person = Person { + name: "Silvia".to_string(), + age: 27, + gender: Gender::Female, + }; + + assert_that(person) + .extracting_ref(|p| p.name()) + .contains("via"); +} + +#[test] +fn extracting_ref_to_assert_all_person_fields() { + let person = Person { + name: "Silvia".to_string(), + age: 27, + gender: Gender::PreferNotToSay, + }; + + assert_that(person) + .extracting_ref(|p| &p.name) + .is_equal_to("Silvia") + .and() + .extracting_ref(|p| &p.age) + .is_at_least(18) + .and() + .extracting_ref(|p| &p.gender) + .is_equal_to(Gender::PreferNotToSay); +} + +#[test] +fn verify_extracting_ref_to_assert_all_fields_fails_with_all_failures() { + let person = Person { + name: "silvia".to_string(), + age: 17, + gender: Gender::NonBinary, + }; + + let failures = verify_that(person) + .extracting_ref(|p| &p.name) + .named("person.name") + .is_equal_to("Silvia") + .and() + .extracting_ref(|p| &p.age) + .named("person.age") + .is_at_least(18) + .and() + .extracting_ref(|p| &p.gender) + .named("person.gender") + .is_equal_to(Gender::PreferNotToSay) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected person.name to be equal to "Silvia" + but was: "silvia" + expected: "Silvia" +"#, + r"expected person.age to be at least 18 + but was: 17 + expected: >= 18 +", + r"expected person.gender to be equal to PreferNotToSay + but was: NonBinary + expected: PreferNotToSay +", + ] + ); +} + +#[cfg(feature = "float-cmp")] +#[test] +fn extracting_ref_to_assert_all_order_item_fields() { + let order = Order { + id: "019d359f-d2f1-7d64-826e-c111ae12dd24".to_string(), + purchased_at: datetime!(2026-03-28 14:20:33 +01:00), + items: vec![ + Item { + name: "Apple".to_string(), + price: 1.99, + quantity: 6, + }, + Item { + name: "Orange".to_string(), + price: 2.99, + quantity: 3, + }, + ], + vat: 0.15, + }; + + assert_that(order) + .extracting_ref(|o| &o.id) + .named("order.id") + .is_not_empty() + .and() + .extracting_ref(|o| &o.purchased_at) + .is_between( + datetime!(2026-03-28 14:00 +01:00), + datetime!(2026-03-28 15:00 +01:00), + ) + .and() + .extracting_ref(|o| &o.items) + .named("order.items") + .has_length(2) + .extracting_ref(|items| &items[0]) + .named("order.items[0]") + .extracting_ref(|i| &i.name) + .named("order.items[0].name") + .is_equal_to("Apple") + .and() + .extracting_ref(|i| &i.price) + .named("order.items[0].price") + .is_close_to(1.99) + .and() + .extracting_ref(|i| &i.quantity) + .is_equal_to(6) + .and() + .and() + .contains_exactly([ + Item { + name: "Apple".to_string(), + price: 1.99, + quantity: 6, + }, + Item { + name: "Orange".to_string(), + price: 2.99, + quantity: 3, + }, + ]) + .and() + .extracting_ref(|o| &o.vat) + .named("order.vat") + .is_close_to(0.15); +} + +#[test] +fn extracting_ref_string_is_equal_to() { + struct Name(String); + + let name = Name("Alexander".to_string()); + + assert_that(name) + .extracting_ref(|n| &n.0) + .is_equal_to("Alexander"); +} + +#[test] +fn extracting_ref_string_is_same_as() { + struct Name(String); + + let name = Name("Alexander".to_string()); + + assert_that(name) + .extracting_ref(|n| &n.0) + .is_same_as("Alexander".to_string()); +} + +#[test] +fn extracting_ref_i32_is_zero() { + struct Int(i32); + + let number = Int(0); + + assert_that(number).extracting_ref(|n| &n.0).is_zero(); +} + +#[test] +fn extracting_ref_i32_is_one() { + struct Int(i32); + + let number = Int(1); + + assert_that(number).extracting_ref(|n| &n.0).is_one(); +} + +#[test] +fn extracting_ref_i32_is_positive() { + struct Int(i32); + + let number = Int(1); + + assert_that(number).extracting_ref(|n| &n.0).is_positive(); +} + +#[test] +fn extracting_ref_i32_is_negative() { + struct Int(i32); + + let number = Int(-1); + + assert_that(number).extracting_ref(|n| &n.0).is_negative(); +} + +#[test] +fn extracting_ref_i32_is_not_positive_and_is_not_negative() { + struct Int(i32); + + let number = Int(0); + + assert_that(number) + .extracting_ref(|n| &n.0) + .is_not_positive() + .is_not_negative(); +} + +#[test] +fn extracting_ref_i32_is_in_range() { + struct Int(i32); + + let number = Int(9); + + assert_that(number) + .extracting_ref(|n| &n.0) + .is_in_range(1..=9); +} + +#[cfg(feature = "float-cmp")] +#[test] +fn extracting_ref_f32_is_close_to() { + struct Float(f32); + + let value = Float(1.99); + + assert_that(value) + .extracting_ref(|f| &f.0) + .is_close_to(1.99); +} + +#[test] +fn extracting_ref_f32_is_zero() { + struct Float(f32); + + let value = Float(0.); + + assert_that(value).extracting_ref(|f| &f.0).is_zero(); +} + +#[test] +fn extracting_ref_f32_is_one() { + struct Float(f32); + + let value = Float(1.); + + assert_that(value).extracting_ref(|f| &f.0).is_one(); +} + +#[test] +fn extracting_ref_f32_is_positive() { + struct Float(f32); + + let value = Float(1.); + + assert_that(value).extracting_ref(|f| &f.0).is_positive(); +} + +#[test] +fn extracting_ref_f32_is_negative() { + struct Float(f32); + + let value = Float(-1.); + + assert_that(value).extracting_ref(|f| &f.0).is_negative(); +} + +#[test] +fn extracting_ref_f32_is_not_positive_and_is_not_negative() { + struct Float(f32); + + let value = Float(0.); + + assert_that(value) + .extracting_ref(|f| &f.0) + .is_not_positive() + .is_not_negative(); +} + +#[test] +fn extracting_ref_f32_is_infinite() { + struct Float(f32); + + let value = Float(f32::INFINITY); + + assert_that(value).extracting_ref(|f| &f.0).is_infinite(); +} + +#[test] +fn extracting_ref_f32_is_not_a_number() { + struct Float(f32); + + let value = Float(f32::NAN); + + assert_that(value) + .extracting_ref(|f| &f.0) + .is_not_a_number(); +} + +#[cfg(feature = "float-cmp")] +#[test] +fn extracting_ref_f64_is_close_to_within_margin() { + struct Float(f64); + + let value = Float(1.99); + + assert_that(value) + .extracting_ref(|f| &f.0) + .is_close_to_with_margin(1.99, (0.001, 2)); +} + +#[test] +fn extracting_ref_f64_is_zero() { + struct Float(f64); + + let value = Float(0.); + + assert_that(value).extracting_ref(|f| &f.0).is_zero(); +} + +#[test] +fn extracting_ref_f64_is_one() { + struct Float(f64); + + let value = Float(1.); + + assert_that(value).extracting_ref(|f| &f.0).is_one(); +} + +#[test] +fn extracting_ref_f64_is_positive() { + struct Float(f64); + + let value = Float(1.); + + assert_that(value).extracting_ref(|f| &f.0).is_positive(); +} + +#[test] +fn extracting_ref_f64_is_negative() { + struct Float(f64); + + let value = Float(-1.); + + assert_that(value).extracting_ref(|f| &f.0).is_negative(); +} + +#[test] +fn extracting_ref_f64_is_not_positive_and_is_not_negative() { + struct Float(f64); + + let value = Float(0.); + + assert_that(value) + .extracting_ref(|f| &f.0) + .is_not_positive() + .is_not_negative(); +} + +#[test] +fn extracting_ref_f64_is_infinite() { + struct Float(f64); + + let value = Float(f64::INFINITY); + + assert_that(value).extracting_ref(|f| &f.0).is_infinite(); +} + +#[test] +fn extracting_ref_f64_is_not_a_number() { + struct Float(f64); + + let value = Float(f64::NAN); + + assert_that(value) + .extracting_ref(|f| &f.0) + .is_not_a_number(); +} + +#[cfg(feature = "bigdecimal")] +#[test] +fn extracting_ref_bigdecimal_has_scale_of() { + struct DecimalNumber(BigDecimal); + + let number = DecimalNumber( + "23.99182405" + .parse() + .unwrap_or_else(|err| panic!("{}", err)), + ); + + assert_that(number).extracting_ref(|n| &n.0).has_scale_of(8); +} + +#[cfg(feature = "bigdecimal")] +#[test] +fn extracting_ref_bigdecimal_has_precision_of() { + struct DecimalNumber(BigDecimal); + + let number = DecimalNumber( + "4123.99182405" + .parse() + .unwrap_or_else(|err| panic!("{}", err)), + ); + + assert_that(number) + .extracting_ref(|n| &n.0) + .has_precision_of(12); +} + +#[cfg(feature = "bigdecimal")] +#[test] +fn extracting_ref_bigdecimal_is_integer() { + struct DecimalNumber(BigDecimal); + + let number = DecimalNumber("123.0".parse().unwrap_or_else(|err| panic!("{}", err))); + + assert_that(number).extracting_ref(|n| &n.0).is_integer(); +} + +#[test] +fn extracting_ref_bool_is_true() { + struct Flag(bool); + + assert_that(Flag(true)).extracting_ref(|f| &f.0).is_true(); +} + +#[test] +fn extracting_ref_bool_is_false() { + struct Flag(bool); + + assert_that(Flag(false)).extracting_ref(|f| &f.0).is_false(); +} + +#[test] +fn extracting_ref_char_is_lowercase() { + struct Character(char); + + assert_that(Character('r')) + .extracting_ref(|c| &c.0) + .is_lowercase(); +} + +#[test] +fn extracting_ref_char_is_uppercase() { + struct Character(char); + + assert_that(Character('R')) + .extracting_ref(|c| &c.0) + .is_uppercase(); +} + +#[test] +fn extracting_ref_char_is_ascii() { + struct Character(char); + + assert_that(Character('@')) + .extracting_ref(|c| &c.0) + .is_ascii(); +} + +#[test] +fn extracting_ref_char_is_alphabetic() { + struct Character(char); + + let character = Character('Z'); + + assert_that(character) + .extracting_ref(|c| &c.0) + .is_alphabetic(); +} + +#[test] +fn extracting_ref_char_is_alphanumeric() { + struct Character(char); + + assert_that(Character('Z')) + .extracting_ref(|c| &c.0) + .is_alphanumeric(); + + assert_that(Character('5')) + .extracting_ref(|c| &c.0) + .is_alphanumeric(); +} + +#[test] +fn extracting_ref_char_is_control_char() { + struct Character(char); + + assert_that(Character('\t')) + .extracting_ref(|c| &c.0) + .is_control_char(); + + assert_that(Character('\u{1b}')) + .extracting_ref(|c| &c.0) + .is_control_char(); +} + +#[test] +fn extracting_ref_char_is_digit() { + struct Character(char); + + assert_that(Character('0')) + .extracting_ref(|c| &c.0) + .is_digit(10); +} + +#[test] +fn extracting_ref_char_is_whitespace() { + struct Character(char); + + assert_that(Character(' ')) + .extracting_ref(|c| &c.0) + .is_whitespace(); + assert_that(Character('\n')) + .extracting_ref(|c| &c.0) + .is_whitespace(); +} + +#[test] +fn extracting_ref_string_is_empty() { + struct Name(String); + + let name = Name(String::new()); + + assert_that(name).extracting_ref(|n| &n.0).is_empty(); +} + +#[test] +fn extracting_ref_string_is_not_empty() { + struct Name(String); + + let name = Name(" ".to_string()); + + assert_that(name).extracting_ref(|n| &n.0).is_not_empty(); +} + +#[test] +fn extracting_ref_vec_is_empty() { + struct Bytes(Vec); + + let name = Bytes(vec![]); + + assert_that(name).extracting_ref(|n| &n.0).is_empty(); +} + +#[test] +fn extracting_ref_vec_is_not_empty() { + struct Bytes(Vec); + + let name = Bytes(vec![48, 65]); + + assert_that(name).extracting_ref(|n| &n.0).is_not_empty(); +} + +#[test] +fn extracting_ref_string_has_length() { + struct Name(String); + + let name = Name("Alex".to_string()); + + assert_that(name).extracting_ref(|n| &n.0).has_length(4); +} + +#[test] +fn extracting_ref_string_has_char_count() { + struct Text(String); + + let name = Text("imper \u{0180} diet al \u{02AA} \u{01AF} zzril".to_string()); + + assert_that(name) + .extracting_ref(|n| &n.0) + .has_char_count(25); +} + +#[test] +fn extracting_ref_option_some() { + struct Optional(Option); + + let note = Optional(Some("note".to_string())); + + assert_that(note).extracting_ref(|n| &n.0).is_some(); +} + +#[test] +fn extracting_ref_option_none() { + struct Optional(Option); + + let note = Optional(None); + + assert_that(note).extracting_ref(|n| &n.0).is_none(); +} + +#[test] +fn extracting_ref_option_some_is_equal_to() { + struct Optional(Option); + + let note = Optional(Some("a note".to_string())); + + assert_that(note) + .extracting_ref(|n| &n.0) + .some() + .is_equal_to("a note"); +} + +#[test] +fn extracting_ref_option_has_value() { + struct Optional(Option); + + let note = Optional(Some("a note".to_string())); + + assert_that(note) + .extracting_ref(|n| &n.0) + .has_value("a note"); +} + +#[test] +fn extracting_ref_result_is_ok() { + struct Response(Result); + + let response = Response(Ok(-123)); + + assert_that(response).extracting_ref(|r| &r.0).is_ok(); +} + +#[test] +fn extracting_ref_result_is_err() { + struct Response(Result); + + let response = Response(Err("not found".to_string())); + + assert_that(response).extracting_ref(|r| &r.0).is_err(); +} + +#[test] +fn extracting_ref_result_ok_is_negative() { + struct Response(Result); + + let response = Response(Ok(-123)); + + assert_that(response) + .extracting_ref(|r| &r.0) + .ok() + .is_negative(); +} + +#[test] +fn extracting_ref_result_err_is_equal_to() { + struct Response(Result); + + let response = Response(Err("not found".to_string())); + + assert_that(response) + .extracting_ref(|r| &r.0) + .err() + .is_equal_to("not found"); +} + +#[test] +fn extracting_ref_result_has_value() { + struct Response(Result); + + let response = Response(Ok(-123)); + + assert_that(response) + .extracting_ref(|r| &r.0) + .has_value(-123); +} + +#[test] +fn extracting_ref_result_has_error() { + struct Response(Result); + + let response = Response(Err("not found".to_string())); + + assert_that(response) + .extracting_ref(|r| &r.0) + .has_error("not found"); +} + +#[test] +fn extracting_ref_string_contains_char() { + struct Name(String); + + let name = Name("Alexander is here".to_string()); + + assert_that(name).extracting_ref(|n| &n.0).contains('x'); +} + +#[test] +fn extracting_ref_string_contains_any_of_chars() { + struct Name(String); + + let name = Name("Alexander is here".to_string()); + + assert_that(name) + .extracting_ref(|n| &n.0) + .contains_any_of(['a', 'e', 'i', 'o', 'u']); +} + +#[test] +fn extracting_ref_vec_has_length() { + struct Bytes(Vec); + + let bytes = Bytes(vec![1, 2, 3, 4, 5]); + + assert_that(bytes).extracting_ref(|b| &b.0).has_length(5); +} + +#[test] +fn extracting_ref_vec_contains() { + struct Names(Vec); + + let names = Names(vec![ + "Silvia".to_string(), + "Alexander".to_string(), + "Robert".to_string(), + ]); + + assert_that(names) + .extracting_ref(|n| &n.0) + .contains("Alexander"); +} + +#[test] +fn extracting_ref_vec_contains_exactly() { + struct Names(Vec); + + let names = Names(vec![ + "Silvia".to_string(), + "Alexander".to_string(), + "Robert".to_string(), + ]); + + assert_that(names) + .extracting_ref(|n| &n.0) + .contains_exactly(["Silvia", "Alexander", "Robert"]); +} + +#[test] +fn extracting_ref_vec_contains_only() { + struct Names(Vec); + + let names = Names(vec![ + "Silvia".to_string(), + "Alexander".to_string(), + "Robert".to_string(), + ]); + + assert_that(names).extracting_ref(|n| &n.0).contains_only([ + "Silvia", + "Robert", + "Philipp", + "Alexander", + ]); +} + +#[test] +fn extracting_ref_vec_contains_any() { + struct Names(Vec); + + let names = Names(vec![ + "Silvia".to_string(), + "Alexander".to_string(), + "Robert".to_string(), + ]); + + assert_that(names) + .extracting_ref(|n| &n.0) + .contains_any_of(["Robert", "Philipp", "Peter"]); +} + +#[test] +fn extracting_ref_vec_contains_all() { + struct Names(Vec); + + let names = Names(vec![ + "Silvia".to_string(), + "Alexander".to_string(), + "Robert".to_string(), + ]); + + assert_that(names) + .extracting_ref(|n| &n.0) + .contains_all_of(["Robert", "Silvia"]); +} + +#[test] +fn extracting_ref_vec_contains_all_in_order() { + struct Names(Vec); + + let names = Names(vec![ + "Silvia".to_string(), + "Alexander".to_string(), + "Robert".to_string(), + ]); + + assert_that(names) + .extracting_ref(|n| &n.0) + .contains_all_in_order(["Silvia", "Robert"]); +} diff --git a/src/lib.rs b/src/lib.rs index cf2a48f..c454f26 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -906,6 +906,7 @@ pub mod __private { pub mod assertions; pub mod colored; pub mod expectations; +pub mod extracting; pub mod prelude; pub mod properties; #[cfg(feature = "recursive")] diff --git a/src/option/mod.rs b/src/option/mod.rs index 1ef06b9..ac3f3ef 100644 --- a/src/option/mod.rs +++ b/src/option/mod.rs @@ -1,8 +1,6 @@ //! Implementation of assertions for `Option` values. -use crate::assertions::{ - AssertBorrowedOptionValue, AssertHasValue, AssertOption, AssertOptionValue, -}; +use crate::assertions::{AssertHasValue, AssertOption, AssertOptionValue}; use crate::colored::{mark_missing, mark_unexpected}; use crate::expectations::{has_value, is_none, is_some, HasValue, IsNone, IsSome}; use crate::spec::{ @@ -55,7 +53,7 @@ where } } -impl<'a, T, R> AssertBorrowedOptionValue for Spec<'a, &'a Option, R> +impl<'a, T, R> AssertOptionValue for Spec<'a, &'a Option, R> where R: FailingStrategy, { diff --git a/src/prelude.rs b/src/prelude.rs index e900534..070b3f1 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -20,7 +20,7 @@ pub use super::{ colored::{DEFAULT_DIFF_FORMAT, DIFF_FORMAT_NO_HIGHLIGHT}, properties::*, spec::{ - assert_that, verify_that, CollectFailures, DoFail, GetFailures, Location, PanicOnFail, + assert_that, verify_that, And, CollectFailures, DoFail, GetFailures, Location, PanicOnFail, SoftPanic, }, verify_that, diff --git a/src/result/mod.rs b/src/result/mod.rs index 155c098..01be986 100644 --- a/src/result/mod.rs +++ b/src/result/mod.rs @@ -1,8 +1,7 @@ //! Implementation of assertions for `Result` values. use crate::assertions::{ - AssertBorrowedResultValue, AssertHasError, AssertHasErrorMessage, AssertHasValue, AssertResult, - AssertResultValue, + AssertHasError, AssertHasErrorMessage, AssertHasValue, AssertResult, AssertResultValue, }; use crate::colored::{mark_missing, mark_unexpected}; use crate::expectations::{ @@ -74,7 +73,7 @@ where } } -impl<'a, T, E, R> AssertBorrowedResultValue for Spec<'a, &'a Result, R> +impl<'a, T, E, R> AssertResultValue for Spec<'a, &'a Result, R> where T: Debug, E: Debug, diff --git a/src/spec/mod.rs b/src/spec/mod.rs index 2c95f93..8b20f24 100644 --- a/src/spec/mod.rs +++ b/src/spec/mod.rs @@ -2,11 +2,12 @@ use crate::colored; use crate::expectations::satisfies; +use crate::extracting::DerivedSpec; + #[cfg(feature = "recursive")] use crate::recursive_comparison::RecursiveComparison; use crate::std::any; -use crate::std::borrow::Borrow; -use crate::std::borrow::Cow; +use crate::std::borrow::{Borrow, Cow, ToOwned}; use crate::std::error::Error as StdError; use crate::std::fmt::{self, Debug, Display}; use crate::std::format; @@ -724,8 +725,8 @@ impl<'a, S, R> Spec<'a, S, R> { /// value and the expected value. /// /// Note: This method must be called before an assertion method is called to - /// have an effect on the failure message of the assertion as failure - /// messages are formatted immediately when an assertion is executed. + /// affect the failure message of the assertion as failure messages are + /// formatted immediately when an assertion is executed. #[must_use = "a spec does nothing unless an assertion method is called"] pub const fn with_diff_format(mut self, diff_format: DiffFormat) -> Self { self.diff_format = diff_format; @@ -811,11 +812,30 @@ impl<'a, S, R> Spec<'a, S, R> { /// /// ``` #[must_use = "a spec does nothing unless an assertion method is called"] - pub fn extracting(self, extractor: F) -> Spec<'a, U, R> + pub fn extracting(self, extract: F) -> Spec<'a, U, R> where F: FnOnce(S) -> U, { - self.mapping(extractor) + Spec { + subject: extract(self.subject), + expression: Expression::default(), + description: self.description, + location: self.location, + failures: self.failures, + diff_format: self.diff_format, + failing_strategy: self.failing_strategy, + } + } + + pub fn extracting_ref(self, extract: F) -> DerivedSpec<'a, Self, U> + where + F: FnOnce(&S) -> &B, + B: ToOwned + ?Sized, + { + let derived_subject = extract(&self.subject).to_owned(); + let expression = Expression::default(); + let diff_format = self.diff_format.clone(); + DerivedSpec::new(self, derived_subject, expression, diff_format) } /// Maps the current subject to some other value. @@ -854,12 +874,12 @@ impl<'a, S, R> Spec<'a, S, R> { /// assertion. So we map the subject of the type `Point` to a tuple of its /// fields. #[must_use = "a spec does nothing unless an assertion method is called"] - pub fn mapping(self, mapper: F) -> Spec<'a, U, R> + pub fn mapping(self, map: F) -> Spec<'a, U, R> where F: FnOnce(S) -> U, { Spec { - subject: mapper(self.subject), + subject: map(self.subject), expression: self.expression, description: self.description, location: self.location, @@ -1264,6 +1284,12 @@ impl SoftPanic for Spec<'_, S, CollectFailures> { } } +pub trait And { + type Target; + + fn and(self) -> Self::Target; +} + /// Access the assertion-failures collected by a `Spec` or spec-like struct. pub trait GetFailures { /// Returns whether there are assertion failures collected so far. From f187502a3fbffa8add1eec218e33d2ef4352b8c3 Mon Sep 17 00:00:00 2001 From: haraldmaida Date: Sun, 29 Mar 2026 21:17:47 +0200 Subject: [PATCH 02/24] chore: run cargo fmt --- src/extracting/tests.rs | 4 ++-- src/prelude.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/extracting/tests.rs b/src/extracting/tests.rs index 6043468..50c8781 100644 --- a/src/extracting/tests.rs +++ b/src/extracting/tests.rs @@ -5,9 +5,9 @@ use crate::std::vec::Vec; #[cfg(feature = "bigdecimal")] use bigdecimal::BigDecimal; #[cfg(feature = "float-cmp")] -use time::macros::datetime; -#[cfg(feature = "float-cmp")] use time::OffsetDateTime; +#[cfg(feature = "float-cmp")] +use time::macros::datetime; #[cfg(feature = "float-cmp")] #[derive(Debug, Clone, PartialEq)] diff --git a/src/prelude.rs b/src/prelude.rs index 070b3f1..1910572 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -20,8 +20,8 @@ pub use super::{ colored::{DEFAULT_DIFF_FORMAT, DIFF_FORMAT_NO_HIGHLIGHT}, properties::*, spec::{ - assert_that, verify_that, And, CollectFailures, DoFail, GetFailures, Location, PanicOnFail, - SoftPanic, + And, CollectFailures, DoFail, GetFailures, Location, PanicOnFail, SoftPanic, assert_that, + verify_that, }, verify_that, }; From 5000f9cedf05d5f05e02e9f62b08a71e218cac5e Mon Sep 17 00:00:00 2001 From: haraldmaida Date: Mon, 30 Mar 2026 10:05:46 +0200 Subject: [PATCH 03/24] feat: implement the rest of existing assertion traits for `DerivedSpec` --- src/extracting/mod.rs | 158 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 138 insertions(+), 20 deletions(-) diff --git a/src/extracting/mod.rs b/src/extracting/mod.rs index 3917d44..4ecb5b1 100644 --- a/src/extracting/mod.rs +++ b/src/extracting/mod.rs @@ -2,35 +2,38 @@ use crate::assertions::{ AssertBoolean, AssertChar, AssertDebugString, AssertDecimalNumber, AssertDisplayString, AssertEmptiness, AssertEquality, AssertErrorHasSource, AssertHasCharCount, AssertHasDebugString, AssertHasDisplayString, AssertHasError, AssertHasErrorMessage, - AssertHasLength, AssertHasValue, AssertInRange, AssertInfinity, AssertIsSorted, - AssertIteratorContains, AssertIteratorContainsInAnyOrder, AssertIteratorContainsInOrder, - AssertNotANumber, AssertNumericIdentity, AssertOption, AssertOptionValue, AssertOrder, - AssertResult, AssertResultValue, AssertSameAs, AssertSignum, AssertStringContainsAnyOf, - AssertStringPattern, + AssertHasLength, AssertHasValue, AssertInRange, AssertInfinity, AssertIteratorContains, + AssertIteratorContainsInAnyOrder, AssertIteratorContainsInOrder, AssertMapContainsKey, + AssertMapContainsValue, AssertNotANumber, AssertNumericIdentity, AssertOption, + AssertOptionValue, AssertOrder, AssertOrderedElements, AssertResult, AssertResultValue, + AssertSameAs, AssertSignum, AssertStringContainsAnyOf, AssertStringPattern, }; use crate::expectations::{ error_has_source, error_has_source_message, has_at_least_char_count, has_at_least_length, - has_at_most_char_count, has_at_most_length, has_char_count, has_char_count_greater_than, - has_char_count_in_range, has_char_count_less_than, has_debug_string, has_display_string, - has_error, has_length, has_length_greater_than, has_length_in_range, has_length_less_than, - has_precision_of, has_scale_of, has_value, is_a_number, is_after, is_alphabetic, - is_alphanumeric, is_ascii, is_at_least, is_at_most, is_before, is_between, is_control_char, - is_digit, is_empty, is_equal_to, is_err, is_false, is_finite, is_greater_than, is_in_range, - is_infinite, is_integer, is_less_than, is_lower_case, is_negative, is_none, is_ok, is_one, - is_positive, is_same_as, is_some, is_true, is_upper_case, is_whitespace, is_zero, - iterator_contains, iterator_contains_all_in_order, iterator_contains_all_of, - iterator_contains_any_of, iterator_contains_exactly, iterator_contains_exactly_in_any_order, - iterator_contains_only, iterator_contains_only_once, iterator_contains_sequence, - iterator_ends_with, iterator_starts_with, not, string_contains, string_contains_any_of, - string_ends_with, string_starts_with, + has_at_least_number_of_elements, has_at_most_char_count, has_at_most_length, has_char_count, + has_char_count_greater_than, has_char_count_in_range, has_char_count_less_than, + has_debug_string, has_display_string, has_error, has_length, has_length_greater_than, + has_length_in_range, has_length_less_than, has_precision_of, has_scale_of, has_value, + is_a_number, is_after, is_alphabetic, is_alphanumeric, is_ascii, is_at_least, is_at_most, + is_before, is_between, is_control_char, is_digit, is_empty, is_equal_to, is_err, is_false, + is_finite, is_greater_than, is_in_range, is_infinite, is_integer, is_less_than, is_lower_case, + is_negative, is_none, is_ok, is_one, is_positive, is_same_as, is_some, is_true, is_upper_case, + is_whitespace, is_zero, iterator_contains, iterator_contains_all_in_order, + iterator_contains_all_of, iterator_contains_any_of, iterator_contains_exactly, + iterator_contains_exactly_in_any_order, iterator_contains_only, iterator_contains_only_once, + iterator_contains_sequence, iterator_ends_with, iterator_starts_with, + map_contains_exactly_keys, map_contains_key, map_contains_keys, map_contains_value, + map_contains_values, map_does_not_contain_keys, map_does_not_contain_values, not, + string_contains, string_contains_any_of, string_ends_with, string_starts_with, }; use crate::properties::{ AdditiveIdentityProperty, CharCountProperty, DecimalProperties, DefinedOrderProperty, - InfinityProperty, IsEmptyProperty, IsNanProperty, LengthProperty, + InfinityProperty, IsEmptyProperty, IsNanProperty, LengthProperty, MapProperties, MultiplicativeIdentityProperty, SignumProperty, }; use crate::spec::{ - And, AssertFailure, DiffFormat, DoFail, Expectation, Expression, GetFailures, SoftPanic, + And, AssertFailure, DiffFormat, DoFail, Expectation, Expression, FailingStrategy, GetFailures, + PanicOnFail, SoftPanic, }; use crate::std::borrow::{Cow, ToOwned}; use crate::std::error::Error; @@ -39,6 +42,7 @@ use crate::std::format; use crate::std::ops::RangeBounds; use crate::std::string::{String, ToString}; use crate::std::vec::Vec; +use hashbrown::HashSet; pub struct DerivedSpec<'a, O, S> { original: O, @@ -1201,5 +1205,119 @@ where } } +impl AssertMapContainsKey for DerivedSpec<'_, O, S> +where + S: MapProperties + Debug, + ::Key: PartialEq + Debug, + ::Value: Debug, + E: Debug, + O: DoFail, +{ + fn contains_key(self, expected_key: E) -> Self { + self.expecting(map_contains_key(expected_key)) + } + + fn does_not_contain_key(self, expected_key: E) -> Self { + self.expecting(not(map_contains_key(expected_key))) + } + + fn contains_keys(self, expected_keys: impl IntoIterator) -> Self { + self.expecting(map_contains_keys(expected_keys)) + } + + fn does_not_contain_keys(self, expected_keys: impl IntoIterator) -> Self { + self.expecting(map_does_not_contain_keys(expected_keys)) + } + + fn contains_exactly_keys(self, expected_keys: impl IntoIterator) -> Self { + self.expecting(map_contains_exactly_keys(expected_keys)) + } +} + +impl AssertMapContainsValue for DerivedSpec<'_, O, S> +where + S: MapProperties + Debug, + ::Key: Debug, + ::Value: PartialEq + Debug, + E: Debug, + O: DoFail, +{ + fn contains_value(self, expected_value: E) -> Self { + self.expecting(map_contains_value(expected_value)) + } + + fn does_not_contain_value(self, expected_value: E) -> Self { + self.expecting(not(map_contains_value(expected_value))) + } + + fn contains_values(self, expected_values: impl IntoIterator) -> Self { + self.expecting(map_contains_values(expected_values)) + } + + fn does_not_contain_values(self, expected_values: impl IntoIterator) -> Self { + self.expecting(map_does_not_contain_values(expected_values)) + } +} + +impl<'a, O, S, T> AssertOrderedElements for DerivedSpec<'a, O, S> +where + S: IntoIterator, + ::IntoIter: DefinedOrderProperty, + T: Debug, + O: DoFail + GetFailures, +{ + type SingleElement = DerivedSpec<'a, O, T>; + type MultipleElements = DerivedSpec<'a, O, Vec>; + + fn first_element(self) -> Self::SingleElement { + let spec = self + .mapping(Vec::from_iter) + .expecting(has_at_least_number_of_elements(1)); + if spec.has_failures() { + PanicOnFail.do_fail_with(&spec.failures()); + unreachable!("Assertion failed and should have panicked! Please report a bug.") + } + spec.extracting(|mut collection| collection.remove(0)) + } + + fn last_element(self) -> Self::SingleElement { + let spec = self + .mapping(Vec::from_iter) + .expecting(has_at_least_number_of_elements(1)); + if spec.has_failures() { + PanicOnFail.do_fail_with(&spec.failures()); + unreachable!("Assertion failed and should have panicked! Please report a bug.") + } + spec.extracting(|mut collection| { + collection.pop().unwrap_or_else(|| { + unreachable!("Assertion failed and should have panicked! Please report a bug.") + }) + }) + } + + fn nth_element(self, n: usize) -> Self::SingleElement { + let min_len = n + 1; + let spec = self + .mapping(Vec::from_iter) + .expecting(has_at_least_number_of_elements(min_len)); + if spec.has_failures() { + PanicOnFail.do_fail_with(&spec.failures()); + unreachable!("Assertion failed and should have panicked! Please report a bug.") + } + spec.extracting(|mut collection| collection.remove(n)) + } + + fn elements_at(self, indices: impl IntoIterator) -> Self::MultipleElements { + let indices = HashSet::<_>::from_iter(indices); + self.mapping(|subject| { + subject + .into_iter() + .enumerate() + .filter_map(|(i, v)| if indices.contains(&i) { Some(v) } else { None }) + .collect() + }) + } +} + #[cfg(test)] mod tests; From 4f2395b62fbe5586b898a88f6698dfc2677e5ddf Mon Sep 17 00:00:00 2001 From: haraldmaida Date: Mon, 30 Mar 2026 10:07:07 +0200 Subject: [PATCH 04/24] revert: remove method `with_configured_diff_format` from `DerivedSpec` - it should be called only once at the root `Spec` --- src/extracting/mod.rs | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/extracting/mod.rs b/src/extracting/mod.rs index 4ecb5b1..a693a1b 100644 --- a/src/extracting/mod.rs +++ b/src/extracting/mod.rs @@ -114,29 +114,6 @@ impl<'a, O, S> DerivedSpec<'a, O, S> { self.diff_format = diff_format; self } - - /// Sets the diff format used to highlight differences between the actual - /// value and the expected value according to the configured mode. - /// - /// The mode is configured via environment variables as described in the - /// module [colored]. - #[cfg(feature = "colored")] - #[cfg_attr(docsrs, doc(cfg(feature = "colored")))] - #[must_use = "a spec does nothing unless an assertion method is called"] - pub fn with_configured_diff_format(self) -> Self { - use crate::colored::configured_diff_format; - #[cfg(not(feature = "std"))] - { - self.with_diff_format(configured_diff_format()) - } - #[cfg(feature = "std")] - { - use crate::std::sync::OnceLock; - static DIFF_FORMAT: OnceLock = OnceLock::new(); - let diff_format = DIFF_FORMAT.get_or_init(configured_diff_format); - self.with_diff_format(diff_format.clone()) - } - } } impl DoFail for DerivedSpec<'_, O, S> From f670bcf703f460bb2d7e1c90679f52d0a62dcc78 Mon Sep 17 00:00:00 2001 From: haraldmaida Date: Mon, 30 Mar 2026 10:32:02 +0200 Subject: [PATCH 05/24] feat: implement the `And` trait for `Spec` as a no-op --- src/spec/mod.rs | 8 ++++++++ src/spec/tests.rs | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/spec/mod.rs b/src/spec/mod.rs index 8b20f24..f6e31b5 100644 --- a/src/spec/mod.rs +++ b/src/spec/mod.rs @@ -1290,6 +1290,14 @@ pub trait And { fn and(self) -> Self::Target; } +impl And for Spec<'_, S, R> { + type Target = Self; + + fn and(self) -> Self::Target { + self + } +} + /// Access the assertion-failures collected by a `Spec` or spec-like struct. pub trait GetFailures { /// Returns whether there are assertion failures collected so far. diff --git a/src/spec/tests.rs b/src/spec/tests.rs index 54ddc64..d0d6994 100644 --- a/src/spec/tests.rs +++ b/src/spec/tests.rs @@ -1,5 +1,6 @@ use crate::prelude::*; use crate::spec::{AssertFailure, Expression, OwnedLocation}; +use crate::std::any::type_name_of_val; use crate::std::{ format, string::{String, ToString}, @@ -230,6 +231,29 @@ fn soft_assertions_panic_once_with_multiple_failure_messages() { .soft_panic(); } +#[cfg(feature = "colored")] +#[test] +fn and_called_on_spec_does_nothing() { + let subject = "the answer to all important questions is 42".to_string(); + + let original_spec = verify_that(subject) + .named("answer") + .with_diff_format(DIFF_FORMAT_RED_BLUE) + .is_empty(); + let original_spec_type = type_name_of_val(&original_spec); + let original_subject = original_spec.subject().clone(); + let original_diff_format = original_spec.diff_format().clone(); + let original_failures = original_spec.failures(); + assert!(!original_failures.is_empty()); + + let returned_spec = original_spec.and(); + + assert_eq!(type_name_of_val(&returned_spec), original_spec_type); + assert_eq!(returned_spec.subject(), &original_subject); + assert_eq!(returned_spec.diff_format(), &original_diff_format); + assert_eq!(returned_spec.failures(), original_failures); +} + #[derive(Debug)] struct TestPerson { name: String, From a1f4bf9674ef978ed3874592d94d9a7f61f9cf8d Mon Sep 17 00:00:00 2001 From: haraldmaida Date: Mon, 30 Mar 2026 10:50:07 +0200 Subject: [PATCH 06/24] refactor: rename module `extracting` to `derived_spec` --- src/{extracting => derived_spec}/mod.rs | 2 +- src/{extracting => derived_spec}/tests.rs | 0 src/lib.rs | 2 +- src/spec/mod.rs | 3 +-- 4 files changed, 3 insertions(+), 4 deletions(-) rename src/{extracting => derived_spec}/mod.rs (99%) rename src/{extracting => derived_spec}/tests.rs (100%) diff --git a/src/extracting/mod.rs b/src/derived_spec/mod.rs similarity index 99% rename from src/extracting/mod.rs rename to src/derived_spec/mod.rs index a693a1b..912a0ee 100644 --- a/src/extracting/mod.rs +++ b/src/derived_spec/mod.rs @@ -1063,8 +1063,8 @@ where #[cfg(feature = "regex")] mod regex { use crate::assertions::AssertStringMatches; + use crate::derived_spec::DerivedSpec; use crate::expectations::{not, string_matches}; - use crate::extracting::DerivedSpec; use crate::spec::DoFail; use crate::std::fmt::Debug; diff --git a/src/extracting/tests.rs b/src/derived_spec/tests.rs similarity index 100% rename from src/extracting/tests.rs rename to src/derived_spec/tests.rs diff --git a/src/lib.rs b/src/lib.rs index c454f26..769099e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -905,8 +905,8 @@ pub mod __private { pub mod assertions; pub mod colored; +pub mod derived_spec; pub mod expectations; -pub mod extracting; pub mod prelude; pub mod properties; #[cfg(feature = "recursive")] diff --git a/src/spec/mod.rs b/src/spec/mod.rs index f6e31b5..2532f90 100644 --- a/src/spec/mod.rs +++ b/src/spec/mod.rs @@ -1,9 +1,8 @@ //! This is the core of the `asserting` crate. use crate::colored; +use crate::derived_spec::DerivedSpec; use crate::expectations::satisfies; -use crate::extracting::DerivedSpec; - #[cfg(feature = "recursive")] use crate::recursive_comparison::RecursiveComparison; use crate::std::any; From 8073ae14ec94b2568072cc39182ebbc2be6c6f49 Mon Sep 17 00:00:00 2001 From: haraldmaida Date: Mon, 6 Apr 2026 07:08:03 +0200 Subject: [PATCH 07/24] refactor!: extract common method `Spec::expecting` and `DerivedSpec::expecting` as trait `Expecting` BREAKING-CHANGE: the behavior of the trait method `Expecting::expecting` is exactly the same as the original methods `Spec::expecting` and `DerivedSpec::expecting`. The only change in custom code is that maybe the trait has to be imported in scope. The `Expecting` trait is included in the prelude. If the import `use asserting::prelude::` is used, there is no change needed. --- src/boolean/mod.rs | 4 ++- src/char/mod.rs | 4 ++- src/char_count.rs | 2 +- src/derived_spec/mod.rs | 48 +++++++++++++--------------- src/equality.rs | 4 ++- src/error/mod.rs | 4 ++- src/float/mod.rs | 4 ++- src/iterator/mod.rs | 4 +-- src/length.rs | 4 ++- src/lib.rs | 2 +- src/map/mod.rs | 4 ++- src/number.rs | 4 ++- src/option/mod.rs | 2 +- src/order/mod.rs | 4 ++- src/panic/mod.rs | 2 +- src/prelude.rs | 4 +-- src/range/mod.rs | 4 ++- src/result/mod.rs | 2 +- src/spec/mod.rs | 71 ++++++++++++++++++++++++----------------- src/spec/tests.rs | 1 + src/string/mod.rs | 8 +++-- 21 files changed, 110 insertions(+), 76 deletions(-) diff --git a/src/boolean/mod.rs b/src/boolean/mod.rs index 480420b..6f91481 100644 --- a/src/boolean/mod.rs +++ b/src/boolean/mod.rs @@ -3,7 +3,9 @@ use crate::assertions::AssertBoolean; use crate::colored::{mark_missing, mark_unexpected}; use crate::expectations::{IsFalse, IsTrue, is_false, is_true}; -use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec}; +use crate::spec::{ + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, +}; use crate::std::format; use crate::std::string::String; diff --git a/src/char/mod.rs b/src/char/mod.rs index a4fedab..907151c 100644 --- a/src/char/mod.rs +++ b/src/char/mod.rs @@ -5,7 +5,9 @@ use crate::expectations::{ IsWhitespace, is_alphabetic, is_alphanumeric, is_ascii, is_control_char, is_digit, is_lower_case, is_upper_case, is_whitespace, }; -use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec}; +use crate::spec::{ + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, +}; use crate::std::format; use crate::std::string::{String, ToString}; diff --git a/src/char_count.rs b/src/char_count.rs index 8196ba2..1029e0e 100644 --- a/src/char_count.rs +++ b/src/char_count.rs @@ -8,7 +8,7 @@ use crate::expectations::{ has_char_count, has_char_count_greater_than, has_char_count_in_range, has_char_count_less_than, }; use crate::properties::CharCountProperty; -use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Spec}; +use crate::spec::{DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Spec}; use crate::std::fmt::Debug; use crate::std::format; use crate::std::ops::RangeBounds; diff --git a/src/derived_spec/mod.rs b/src/derived_spec/mod.rs index 912a0ee..4f8a370 100644 --- a/src/derived_spec/mod.rs +++ b/src/derived_spec/mod.rs @@ -32,8 +32,8 @@ use crate::properties::{ MultiplicativeIdentityProperty, SignumProperty, }; use crate::spec::{ - And, AssertFailure, DiffFormat, DoFail, Expectation, Expression, FailingStrategy, GetFailures, - PanicOnFail, SoftPanic, + And, AssertFailure, DiffFormat, DoFail, Expectation, Expecting, Expression, FailingStrategy, + GetFailures, PanicOnFail, SoftPanic, }; use crate::std::borrow::{Cow, ToOwned}; use crate::std::error::Error; @@ -51,23 +51,6 @@ pub struct DerivedSpec<'a, O, S> { diff_format: DiffFormat, } -impl GetFailures for DerivedSpec<'_, O, S> -where - O: GetFailures, -{ - fn has_failures(&self) -> bool { - self.original.has_failures() - } - - fn failures(&self) -> Vec { - self.original.failures() - } - - fn display_failures(&self) -> Vec { - self.original.display_failures() - } -} - impl DerivedSpec<'_, O, S> { /// Returns the expression (or subject name) if one has been set. pub fn expression(&self) -> &Expression<'_> { @@ -116,6 +99,23 @@ impl<'a, O, S> DerivedSpec<'a, O, S> { } } +impl GetFailures for DerivedSpec<'_, O, S> +where + O: GetFailures, +{ + fn has_failures(&self) -> bool { + self.original.has_failures() + } + + fn failures(&self) -> Vec { + self.original.failures() + } + + fn display_failures(&self) -> Vec { + self.original.display_failures() + } +} + impl DoFail for DerivedSpec<'_, O, S> where O: DoFail, @@ -194,13 +194,11 @@ impl<'a, O, S> DerivedSpec<'a, O, S> { } } -impl DerivedSpec<'_, O, S> +impl Expecting for DerivedSpec<'_, O, S> where O: DoFail, { - #[allow(clippy::needless_pass_by_value, clippy::return_self_not_must_use)] - #[track_caller] - pub fn expecting(mut self, mut expectation: impl Expectation) -> Self { + fn expecting(mut self, mut expectation: impl Expectation) -> Self { if !expectation.test(&self.subject) { let message = expectation.message(&self.expression, &self.subject, false, &self.diff_format); @@ -244,7 +242,7 @@ mod float_cmp { use super::DerivedSpec; use crate::assertions::{AssertIsCloseToWithDefaultMargin, AssertIsCloseToWithinMargin}; use crate::expectations::{is_close_to, not}; - use crate::spec::DoFail; + use crate::spec::{DoFail, Expecting}; use float_cmp::{F32Margin, F64Margin}; impl AssertIsCloseToWithinMargin for DerivedSpec<'_, O, f32> @@ -1065,7 +1063,7 @@ mod regex { use crate::assertions::AssertStringMatches; use crate::derived_spec::DerivedSpec; use crate::expectations::{not, string_matches}; - use crate::spec::DoFail; + use crate::spec::{DoFail, Expecting}; use crate::std::fmt::Debug; impl AssertStringMatches for DerivedSpec<'_, O, S> diff --git a/src/equality.rs b/src/equality.rs index 3564fdb..eae0280 100644 --- a/src/equality.rs +++ b/src/equality.rs @@ -8,7 +8,9 @@ use crate::expectations::{ HasDebugString, HasDisplayString, IsEqualTo, IsSameAs, has_debug_string, has_display_string, is_equal_to, is_same_as, not, }; -use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec}; +use crate::spec::{ + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, +}; use crate::std::fmt::{Debug, Display}; use crate::std::format; use crate::std::string::{String, ToString}; diff --git a/src/error/mod.rs b/src/error/mod.rs index 9e54ba7..b2aa34f 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -3,7 +3,9 @@ use crate::colored::{mark_missing, mark_missing_string, mark_unexpected, mark_un use crate::expectations::{ ErrorHasSource, ErrorHasSourceMessage, error_has_source, error_has_source_message, not, }; -use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec}; +use crate::spec::{ + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, +}; use crate::std::error::Error; use crate::std::format; use crate::std::string::{String, ToString}; diff --git a/src/float/mod.rs b/src/float/mod.rs index 5b98edc..69f77a6 100644 --- a/src/float/mod.rs +++ b/src/float/mod.rs @@ -98,7 +98,9 @@ mod cmp { use crate::assertions::{AssertIsCloseToWithDefaultMargin, AssertIsCloseToWithinMargin}; use crate::colored::mark_diff; use crate::expectations::{IsCloseTo, is_close_to, not}; - use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec}; + use crate::spec::{ + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, + }; use crate::std::{format, string::String}; use float_cmp::{ApproxEq, F32Margin, F64Margin}; diff --git a/src/iterator/mod.rs b/src/iterator/mod.rs index eb18dfd..165dca4 100644 --- a/src/iterator/mod.rs +++ b/src/iterator/mod.rs @@ -21,8 +21,8 @@ use crate::expectations::{ }; use crate::properties::DefinedOrderProperty; use crate::spec::{ - DiffFormat, Expectation, Expression, FailingStrategy, GetFailures, Invertible, PanicOnFail, - Spec, + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, GetFailures, Invertible, + PanicOnFail, Spec, }; use crate::std::cmp::Ordering; use crate::std::fmt::Debug; diff --git a/src/length.rs b/src/length.rs index 845b7d0..bb2f9fd 100644 --- a/src/length.rs +++ b/src/length.rs @@ -8,7 +8,9 @@ use crate::expectations::{ has_length_greater_than, has_length_in_range, has_length_less_than, is_empty, not, }; use crate::properties::{IsEmptyProperty, LengthProperty}; -use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec}; +use crate::spec::{ + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, +}; use crate::std::fmt::Debug; use crate::std::ops::RangeBounds; use crate::std::{format, string::String}; diff --git a/src/lib.rs b/src/lib.rs index 769099e..bbb79dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -698,7 +698,7 @@ //! # ) //! # } //! # } -//! use asserting::spec::{FailingStrategy, Spec}; +//! use asserting::spec::{Expecting, FailingStrategy, Spec}; //! use std::fmt::Debug; //! //! pub trait AssertEither { diff --git a/src/map/mod.rs b/src/map/mod.rs index 3f9e592..4975a20 100644 --- a/src/map/mod.rs +++ b/src/map/mod.rs @@ -11,7 +11,9 @@ use crate::expectations::{ }; use crate::iterator::collect_selected_values; use crate::properties::MapProperties; -use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec}; +use crate::spec::{ + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, +}; use crate::std::fmt::Debug; use crate::std::format; use crate::std::string::String; diff --git a/src/number.rs b/src/number.rs index f38dbf7..15e0365 100644 --- a/src/number.rs +++ b/src/number.rs @@ -13,7 +13,9 @@ use crate::properties::{ AdditiveIdentityProperty, DecimalProperties, InfinityProperty, IsNanProperty, MultiplicativeIdentityProperty, SignumProperty, }; -use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec}; +use crate::spec::{ + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, +}; use crate::std::fmt::Debug; use crate::std::format; use crate::std::string::String; diff --git a/src/option/mod.rs b/src/option/mod.rs index 2fa496c..5f66877 100644 --- a/src/option/mod.rs +++ b/src/option/mod.rs @@ -4,7 +4,7 @@ use crate::assertions::{AssertHasValue, AssertOption, AssertOptionValue}; use crate::colored::{mark_missing, mark_unexpected}; use crate::expectations::{HasValue, IsNone, IsSome, has_value, is_none, is_some}; use crate::spec::{ - DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec, Unknown, + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, Unknown, }; use crate::std::fmt::Debug; use crate::std::{format, string::String}; diff --git a/src/order/mod.rs b/src/order/mod.rs index 63c906d..c907704 100644 --- a/src/order/mod.rs +++ b/src/order/mod.rs @@ -6,7 +6,9 @@ use crate::expectations::{ IsAfter, IsAtLeast, IsAtMost, IsBefore, IsBetween, IsGreaterThan, IsLessThan, is_after, is_at_least, is_at_most, is_before, is_between, is_greater_than, is_less_than, }; -use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec}; +use crate::spec::{ + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, +}; use crate::std::fmt::Debug; use crate::std::{format, string::String}; diff --git a/src/panic/mod.rs b/src/panic/mod.rs index ff8df1b..3c908c6 100644 --- a/src/panic/mod.rs +++ b/src/panic/mod.rs @@ -3,7 +3,7 @@ use crate::assertions::AssertCodePanics; use crate::colored::{mark_missing_string, mark_unexpected_string}; use crate::expectations::{DoesNotPanic, DoesPanic, does_not_panic, does_panic}; -use crate::spec::{Code, DiffFormat, Expectation, Expression, FailingStrategy, Spec}; +use crate::spec::{Code, DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Spec}; use crate::std::any::Any; use crate::std::panic; diff --git a/src/prelude.rs b/src/prelude.rs index 1910572..cb5f8b7 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -20,8 +20,8 @@ pub use super::{ colored::{DEFAULT_DIFF_FORMAT, DIFF_FORMAT_NO_HIGHLIGHT}, properties::*, spec::{ - And, CollectFailures, DoFail, GetFailures, Location, PanicOnFail, SoftPanic, assert_that, - verify_that, + And, CollectFailures, DoFail, Expecting, GetFailures, Location, PanicOnFail, SoftPanic, + assert_that, verify_that, }, verify_that, }; diff --git a/src/range/mod.rs b/src/range/mod.rs index 22b5727..d564001 100644 --- a/src/range/mod.rs +++ b/src/range/mod.rs @@ -4,7 +4,9 @@ use crate::assertions::AssertInRange; use crate::colored::{mark_missing, mark_missing_string, mark_unexpected}; use crate::expectations::{IsInRange, is_in_range, not}; use crate::properties::IsEmptyProperty; -use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec}; +use crate::spec::{ + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, +}; use crate::std::fmt::Debug; use crate::std::format; use crate::std::ops::{Bound, Range, RangeBounds, RangeInclusive}; diff --git a/src/result/mod.rs b/src/result/mod.rs index 32eec5b..057a060 100644 --- a/src/result/mod.rs +++ b/src/result/mod.rs @@ -8,7 +8,7 @@ use crate::expectations::{ HasError, HasValue, IsErr, IsOk, has_error, has_value, is_equal_to, is_err, is_ok, }; use crate::spec::{ - DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec, Unknown, + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, Unknown, }; use crate::std::fmt::{Debug, Display}; use crate::std::{ diff --git a/src/spec/mod.rs b/src/spec/mod.rs index 2532f90..ddbe9f5 100644 --- a/src/spec/mod.rs +++ b/src/spec/mod.rs @@ -893,36 +893,6 @@ impl Spec<'_, S, R> where R: FailingStrategy, { - /// Asserts the given expectation. - /// - /// In case the expectation is not meet, the assertion fails according to - /// the current failing strategy of this `Spec`. - /// - /// This method is called from the implementations of the assertion traits - /// defined in the [`assertions`](crate::assertions) module. Implementations - /// of custom assertions will call this method with a proper expectation. - /// - /// # Examples - /// - /// ``` - /// use asserting::expectations::{IsEmpty, IsEqualTo}; - /// use asserting::prelude::*; - /// - /// assert_that!(7 * 6).expecting(IsEqualTo {expected: 42 }); - /// - /// assert_that!("").expecting(IsEmpty); - /// ``` - #[allow(clippy::needless_pass_by_value, clippy::return_self_not_must_use)] - #[track_caller] - pub fn expecting(mut self, mut expectation: impl Expectation) -> Self { - if !expectation.test(&self.subject) { - let message = - expectation.message(&self.expression, &self.subject, false, &self.diff_format); - self.do_fail_with_message(message); - } - self - } - /// Asserts whether the given predicate is meet. /// /// This method takes a predicate function and calls it as an expectation. @@ -1297,6 +1267,47 @@ impl And for Spec<'_, S, R> { } } +/// Verify whether a subject meets the given expectation (impl of +/// [`Expectation`]) and record a failure if it is not met. +pub trait Expecting { + /// Asserts the given expectation. + /// + /// In case the expectation is not meet, the assertion fails, according to + /// the current failing strategy of this `Spec`. + /// + /// This method is called from the implementations of the assertion traits + /// defined in the [`assertions`](crate::assertions) module. Implementations + /// of custom assertions will call this method with a proper expectation. + /// + /// # Examples + /// + /// ``` + /// use asserting::expectations::{IsEmpty, IsEqualTo}; + /// use asserting::prelude::*; + /// + /// assert_that!(7 * 6).expecting(IsEqualTo {expected: 42 }); + /// + /// assert_that!("").expecting(IsEmpty); + /// ``` + #[allow(clippy::needless_pass_by_value, clippy::return_self_not_must_use)] + #[track_caller] + fn expecting(self, expectation: impl Expectation) -> Self; +} + +impl Expecting for Spec<'_, S, R> +where + R: FailingStrategy, +{ + fn expecting(mut self, mut expectation: impl Expectation) -> Self { + if !expectation.test(&self.subject) { + let message = + expectation.message(&self.expression, &self.subject, false, &self.diff_format); + self.do_fail_with_message(message); + } + self + } +} + /// Access the assertion-failures collected by a `Spec` or spec-like struct. pub trait GetFailures { /// Returns whether there are assertion failures collected so far. diff --git a/src/spec/tests.rs b/src/spec/tests.rs index d0d6994..1e3568e 100644 --- a/src/spec/tests.rs +++ b/src/spec/tests.rs @@ -1,5 +1,6 @@ use crate::prelude::*; use crate::spec::{AssertFailure, Expression, OwnedLocation}; +#[cfg(feature = "colored")] use crate::std::any::type_name_of_val; use crate::std::{ format, diff --git a/src/string/mod.rs b/src/string/mod.rs index dca10b7..f77ed67 100644 --- a/src/string/mod.rs +++ b/src/string/mod.rs @@ -11,7 +11,9 @@ use crate::expectations::{ string_contains_any_of, string_ends_with, string_starts_with, }; use crate::properties::{CharCountProperty, DefinedOrderProperty, IsEmptyProperty, LengthProperty}; -use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec}; +use crate::spec::{ + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, +}; use crate::std::fmt::Debug; use crate::std::str::Chars; use crate::std::{ @@ -703,7 +705,9 @@ mod regex { use crate::assertions::AssertStringMatches; use crate::colored::{mark_missing_string, mark_unexpected_string}; use crate::expectations::{StringMatches, not, string_matches}; - use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec}; + use crate::spec::{ + DiffFormat, Expectation, Expecting, Expression, FailingStrategy, Invertible, Spec, + }; use crate::std::fmt::Debug; use crate::std::format; use crate::std::string::String; From 676106b0e9011e95805bae6b1287bb042dd2083a Mon Sep 17 00:00:00 2001 From: haraldmaida Date: Mon, 6 Apr 2026 07:14:09 +0200 Subject: [PATCH 08/24] refactor: rename `And::Target` to `And::Output` --- src/derived_spec/mod.rs | 4 ++-- src/spec/mod.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/derived_spec/mod.rs b/src/derived_spec/mod.rs index 4f8a370..86ea790 100644 --- a/src/derived_spec/mod.rs +++ b/src/derived_spec/mod.rs @@ -139,9 +139,9 @@ where } impl And for DerivedSpec<'_, O, S> { - type Target = O; + type Output = O; - fn and(self) -> Self::Target { + fn and(self) -> Self::Output { self.original } } diff --git a/src/spec/mod.rs b/src/spec/mod.rs index ddbe9f5..4c3eb34 100644 --- a/src/spec/mod.rs +++ b/src/spec/mod.rs @@ -1256,13 +1256,13 @@ impl SoftPanic for Spec<'_, S, CollectFailures> { pub trait And { type Target; - fn and(self) -> Self::Target; + fn and(self) -> Self::Output; } impl And for Spec<'_, S, R> { - type Target = Self; + type Output = Self; - fn and(self) -> Self::Target { + fn and(self) -> Self::Output { self } } From f2c12876502773e55f28554e11a9d16a46879b12 Mon Sep 17 00:00:00 2001 From: haraldmaida Date: Mon, 6 Apr 2026 08:13:34 +0200 Subject: [PATCH 09/24] doc: write doc comment for the `derived_spec` module and the `And` trait --- src/derived_spec/mod.rs | 12 ++++++ src/derived_spec/tests.rs | 2 +- src/spec/mod.rs | 91 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 103 insertions(+), 2 deletions(-) diff --git a/src/derived_spec/mod.rs b/src/derived_spec/mod.rs index 86ea790..84eaf22 100644 --- a/src/derived_spec/mod.rs +++ b/src/derived_spec/mod.rs @@ -1,3 +1,6 @@ +//! Defines the [`DerivedSpec`], which keeps track of the original subject while doing assertions +//! on a derived subject. + use crate::assertions::{ AssertBoolean, AssertChar, AssertDebugString, AssertDecimalNumber, AssertDisplayString, AssertEmptiness, AssertEquality, AssertErrorHasSource, AssertHasCharCount, @@ -44,6 +47,15 @@ use crate::std::string::{String, ToString}; use crate::std::vec::Vec; use hashbrown::HashSet; +/// A `DerivedSpec` does assertions on a derived subject while keeping track +/// of the original subject. +/// +/// It has similar functionality as a `Spec`, but additionally holds the +/// original subject. Calling the `and` method switches the subject back to the +/// original subject. +/// +/// The derived subject can have its own name and diff format in failure +/// reports. pub struct DerivedSpec<'a, O, S> { original: O, subject: S, diff --git a/src/derived_spec/tests.rs b/src/derived_spec/tests.rs index 50c8781..ed44e82 100644 --- a/src/derived_spec/tests.rs +++ b/src/derived_spec/tests.rs @@ -76,7 +76,7 @@ fn extracting_ref_person_name_via_accessor_contains_via() { }; assert_that(person) - .extracting_ref(|p| p.name()) + .extracting_ref(Person::name) .contains("via"); } diff --git a/src/spec/mod.rs b/src/spec/mod.rs index 4c3eb34..2cd8d66 100644 --- a/src/spec/mod.rs +++ b/src/spec/mod.rs @@ -1253,9 +1253,98 @@ impl SoftPanic for Spec<'_, S, CollectFailures> { } } +/// Chaining another assertion. +/// +/// Both the previous assertion and the next assertion must be met to pass the +/// overall assertion. pub trait And { - type Target; + /// The return type of the `and()` method. + type Output; + /// Express explicitly that another assertion must be met to pass the + /// overall assertion. + /// + /// Note: assertions can be changed anyway without calling this `and` + /// method. So in most cases, this method does nothing and just offers a + /// different style of writing assertions. + /// + /// In combination with the [`Spec::extracting_ref`] the `and` method can be + /// used to chain multiple assertions on the original subject, instead of + /// the extracted one. + /// + /// # Examples + /// + /// Calling the `and` method on the original subject is optional and just + /// a question of style how one wants to write assertions. + /// + /// ``` + /// use asserting::prelude::*; + /// + /// let subject = "the answer to all important questions in the universe is 42"; + /// + /// assert_that(subject).is_not_empty() + /// .and().contains("answer to all important questions") + /// .and().ends_with("42"); + /// + /// // the same assertions can be written without calling `and()`: + /// + /// assert_that(subject).is_not_empty() + /// .contains("answer to all important questions") + /// .ends_with("42"); + /// ``` + /// + /// In combination with the [`Spec::extracting_ref`] method, the `and` method + /// switches back to the original subject to chain multiple assertions on + /// different extracted fields. + /// + /// ``` + /// use asserting::prelude::*; + /// + /// #[derive(Debug, Clone, Copy, PartialEq)] + /// enum Gender { + /// Male, + /// Female, + /// NonBinary, + /// PreferNotToSay, + /// } + /// + /// struct Person { + /// name: String, + /// age: u8, + /// gender: Gender, + /// } + /// + /// impl Person { + /// fn name(&self) -> &str { + /// &self.name + /// } + /// } + /// + /// let my_friend = Person { + /// name: "Silvia".into(), + /// age: 27, + /// gender: Gender::Female, + /// }; + /// + /// assert_that!(my_friend) + /// .extracting_ref(Person::name) + /// .named("my_friend.name") + /// .is_equal_to("Silvia") + /// .and() + /// .extracting_ref(|p| &p.age) + /// .named("my_friend.age") + /// .is_at_least(18) + /// .and() + /// .extracting_ref(|p| &p.gender) + /// .named("my_friend.gender") + /// .is_equal_to(Gender::Female); + /// ``` + /// + /// Calling the `named` method after extracting a field is optional but + /// helps in case of a failing assertion as the failure report references + /// the more detailed name, such as `my_friend.name` or `my_friend.age`, + /// instead of just `my_friend`. + #[must_use = "calling the `and` method without calling another assertion method is useless"] fn and(self) -> Self::Output; } From 2f697d281a6055ec3f582de8687581a846bfd893 Mon Sep 17 00:00:00 2001 From: haraldmaida Date: Mon, 6 Apr 2026 08:30:34 +0200 Subject: [PATCH 10/24] doc: adapt doc comment for `Spec::extracting` method to changed behavior --- src/spec/mod.rs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/spec/mod.rs b/src/spec/mod.rs index 2cd8d66..891bf42 100644 --- a/src/spec/mod.rs +++ b/src/spec/mod.rs @@ -780,16 +780,21 @@ impl<'a, S, R> Spec<'a, S, R> { /// It takes a closure that maps the current subject to a new subject and /// returns a new `Spec` with the value returned by the closure as the new /// subject. The new subject may have a different type than the original - /// subject. All other data like expression, description, and location are + /// subject. All other data like description, location, and diff format are /// taken over from this `Spec` into the returned `Spec`. /// /// This function is useful when having a custom type, and a specific /// property of this type shall be asserted only. /// - /// This is an alias function to the [`mapping()`](Spec::mapping) function. - /// Both functions do exactly the same. The idea is to provide different - /// names to be able to express the intent more clearly when used in - /// assertions. + /// This method is similar to the [`mapping()`](Spec::mapping) method. In + /// contrast to [`mapping()`](Spec::mapping), this method does not copy the + /// subject's name (or expression) but resets it to the default "subject". + /// The idea is that the "extracted" property is definitely a different + /// subject than the original one. + /// + /// It is recommended to give the extracted property a specific name by + /// calling the `named` method. This helps with spotting the cause of a + /// failing assertion. /// /// # Example /// @@ -806,7 +811,9 @@ impl<'a, S, R> Spec<'a, S, R> { /// other_property: 99.9, /// }; /// - /// assert_that!(some_thing).extracting(|s| s.important_property) + /// assert_that!(some_thing) + /// .extracting(|s| s.important_property) + /// .named("some_thing.important_property") /// .is_equal_to("imperdiet aliqua zzril eiusmod"); /// /// ``` @@ -1343,7 +1350,7 @@ pub trait And { /// Calling the `named` method after extracting a field is optional but /// helps in case of a failing assertion as the failure report references /// the more detailed name, such as `my_friend.name` or `my_friend.age`, - /// instead of just `my_friend`. + /// instead of just `subject`. #[must_use = "calling the `and` method without calling another assertion method is useless"] fn and(self) -> Self::Output; } From e13efde1a669bcd2213f91fddc076b1712864747 Mon Sep 17 00:00:00 2001 From: haraldmaida Date: Mon, 6 Apr 2026 09:01:54 +0200 Subject: [PATCH 11/24] feat: add parameter `property_name` to the `Spec::extracting_ref` and `DerivedSpec::extracting_ref` methods --- src/derived_spec/mod.rs | 8 +- src/derived_spec/tests.rs | 238 ++++++++++++++++++++++---------------- src/spec/mod.rs | 24 ++-- 3 files changed, 159 insertions(+), 111 deletions(-) diff --git a/src/derived_spec/mod.rs b/src/derived_spec/mod.rs index 84eaf22..bb5b543 100644 --- a/src/derived_spec/mod.rs +++ b/src/derived_spec/mod.rs @@ -160,13 +160,17 @@ impl And for DerivedSpec<'_, O, S> { impl<'a, O, S> DerivedSpec<'a, O, S> { #[must_use = "a derived spec does nothing unless an assertion method is called"] - pub fn extracting_ref(self, extract: F) -> DerivedSpec<'a, Self, U> + pub fn extracting_ref( + self, + property_name: impl Into>, + extract: F, + ) -> DerivedSpec<'a, Self, U> where F: FnOnce(&S) -> &B, B: ToOwned + ?Sized, { let extracted = extract(&self.subject).to_owned(); - let expression = Expression::default(); + let expression = Expression(property_name.into()); let diff_format = self.diff_format.clone(); DerivedSpec { original: self, diff --git a/src/derived_spec/tests.rs b/src/derived_spec/tests.rs index ed44e82..cea53b8 100644 --- a/src/derived_spec/tests.rs +++ b/src/derived_spec/tests.rs @@ -76,7 +76,7 @@ fn extracting_ref_person_name_via_accessor_contains_via() { }; assert_that(person) - .extracting_ref(Person::name) + .extracting_ref("person.name", Person::name) .contains("via"); } @@ -89,13 +89,13 @@ fn extracting_ref_to_assert_all_person_fields() { }; assert_that(person) - .extracting_ref(|p| &p.name) + .extracting_ref("person.name", |p| &p.name) .is_equal_to("Silvia") .and() - .extracting_ref(|p| &p.age) + .extracting_ref("person.age", |p| &p.age) .is_at_least(18) .and() - .extracting_ref(|p| &p.gender) + .extracting_ref("person.gender", |p| &p.gender) .is_equal_to(Gender::PreferNotToSay); } @@ -108,16 +108,13 @@ fn verify_extracting_ref_to_assert_all_fields_fails_with_all_failures() { }; let failures = verify_that(person) - .extracting_ref(|p| &p.name) - .named("person.name") + .extracting_ref("person.name", Person::name) .is_equal_to("Silvia") .and() - .extracting_ref(|p| &p.age) - .named("person.age") + .extracting_ref("person.age", |p| &p.age) .is_at_least(18) .and() - .extracting_ref(|p| &p.gender) - .named("person.gender") + .extracting_ref("person.gender", |p| &p.gender) .is_equal_to(Gender::PreferNotToSay) .display_failures(); @@ -162,30 +159,25 @@ fn extracting_ref_to_assert_all_order_item_fields() { }; assert_that(order) - .extracting_ref(|o| &o.id) - .named("order.id") + .extracting_ref("order.id", |o| &o.id) .is_not_empty() .and() - .extracting_ref(|o| &o.purchased_at) + .extracting_ref("order.purchased_at", |o| &o.purchased_at) .is_between( datetime!(2026-03-28 14:00 +01:00), datetime!(2026-03-28 15:00 +01:00), ) .and() - .extracting_ref(|o| &o.items) - .named("order.items") + .extracting_ref("order.items", |o| &o.items) .has_length(2) - .extracting_ref(|items| &items[0]) - .named("order.items[0]") - .extracting_ref(|i| &i.name) - .named("order.items[0].name") + .extracting_ref("order.items[0]", |items| &items[0]) + .extracting_ref("order.items[0].name", |i| &i.name) .is_equal_to("Apple") .and() - .extracting_ref(|i| &i.price) - .named("order.items[0].price") + .extracting_ref("order.items[0].price", |i| &i.price) .is_close_to(1.99) .and() - .extracting_ref(|i| &i.quantity) + .extracting_ref("order.items[0].quantity", |i| &i.quantity) .is_equal_to(6) .and() .and() @@ -202,8 +194,7 @@ fn extracting_ref_to_assert_all_order_item_fields() { }, ]) .and() - .extracting_ref(|o| &o.vat) - .named("order.vat") + .extracting_ref("order.vat", |o| &o.vat) .is_close_to(0.15); } @@ -214,7 +205,7 @@ fn extracting_ref_string_is_equal_to() { let name = Name("Alexander".to_string()); assert_that(name) - .extracting_ref(|n| &n.0) + .extracting_ref("name.0", |n| &n.0) .is_equal_to("Alexander"); } @@ -225,7 +216,7 @@ fn extracting_ref_string_is_same_as() { let name = Name("Alexander".to_string()); assert_that(name) - .extracting_ref(|n| &n.0) + .extracting_ref("name.0", |n| &n.0) .is_same_as("Alexander".to_string()); } @@ -235,7 +226,9 @@ fn extracting_ref_i32_is_zero() { let number = Int(0); - assert_that(number).extracting_ref(|n| &n.0).is_zero(); + assert_that(number) + .extracting_ref("number.0", |n| &n.0) + .is_zero(); } #[test] @@ -244,7 +237,9 @@ fn extracting_ref_i32_is_one() { let number = Int(1); - assert_that(number).extracting_ref(|n| &n.0).is_one(); + assert_that(number) + .extracting_ref("number.0", |n| &n.0) + .is_one(); } #[test] @@ -253,7 +248,9 @@ fn extracting_ref_i32_is_positive() { let number = Int(1); - assert_that(number).extracting_ref(|n| &n.0).is_positive(); + assert_that(number) + .extracting_ref("number.0", |n| &n.0) + .is_positive(); } #[test] @@ -262,7 +259,9 @@ fn extracting_ref_i32_is_negative() { let number = Int(-1); - assert_that(number).extracting_ref(|n| &n.0).is_negative(); + assert_that(number) + .extracting_ref("number.0", |n| &n.0) + .is_negative(); } #[test] @@ -272,7 +271,7 @@ fn extracting_ref_i32_is_not_positive_and_is_not_negative() { let number = Int(0); assert_that(number) - .extracting_ref(|n| &n.0) + .extracting_ref("number.0", |n| &n.0) .is_not_positive() .is_not_negative(); } @@ -284,7 +283,7 @@ fn extracting_ref_i32_is_in_range() { let number = Int(9); assert_that(number) - .extracting_ref(|n| &n.0) + .extracting_ref("number.0", |n| &n.0) .is_in_range(1..=9); } @@ -296,7 +295,7 @@ fn extracting_ref_f32_is_close_to() { let value = Float(1.99); assert_that(value) - .extracting_ref(|f| &f.0) + .extracting_ref("float.0", |f| &f.0) .is_close_to(1.99); } @@ -306,7 +305,9 @@ fn extracting_ref_f32_is_zero() { let value = Float(0.); - assert_that(value).extracting_ref(|f| &f.0).is_zero(); + assert_that(value) + .extracting_ref("float.0", |f| &f.0) + .is_zero(); } #[test] @@ -315,7 +316,9 @@ fn extracting_ref_f32_is_one() { let value = Float(1.); - assert_that(value).extracting_ref(|f| &f.0).is_one(); + assert_that(value) + .extracting_ref("float.0", |f| &f.0) + .is_one(); } #[test] @@ -324,7 +327,9 @@ fn extracting_ref_f32_is_positive() { let value = Float(1.); - assert_that(value).extracting_ref(|f| &f.0).is_positive(); + assert_that(value) + .extracting_ref("float.0", |f| &f.0) + .is_positive(); } #[test] @@ -333,7 +338,9 @@ fn extracting_ref_f32_is_negative() { let value = Float(-1.); - assert_that(value).extracting_ref(|f| &f.0).is_negative(); + assert_that(value) + .extracting_ref("float.0", |f| &f.0) + .is_negative(); } #[test] @@ -343,7 +350,7 @@ fn extracting_ref_f32_is_not_positive_and_is_not_negative() { let value = Float(0.); assert_that(value) - .extracting_ref(|f| &f.0) + .extracting_ref("float.0", |f| &f.0) .is_not_positive() .is_not_negative(); } @@ -354,7 +361,9 @@ fn extracting_ref_f32_is_infinite() { let value = Float(f32::INFINITY); - assert_that(value).extracting_ref(|f| &f.0).is_infinite(); + assert_that(value) + .extracting_ref("float.0", |f| &f.0) + .is_infinite(); } #[test] @@ -364,7 +373,7 @@ fn extracting_ref_f32_is_not_a_number() { let value = Float(f32::NAN); assert_that(value) - .extracting_ref(|f| &f.0) + .extracting_ref("float.0", |f| &f.0) .is_not_a_number(); } @@ -376,7 +385,7 @@ fn extracting_ref_f64_is_close_to_within_margin() { let value = Float(1.99); assert_that(value) - .extracting_ref(|f| &f.0) + .extracting_ref("float.0", |f| &f.0) .is_close_to_with_margin(1.99, (0.001, 2)); } @@ -386,7 +395,9 @@ fn extracting_ref_f64_is_zero() { let value = Float(0.); - assert_that(value).extracting_ref(|f| &f.0).is_zero(); + assert_that(value) + .extracting_ref("float.0", |f| &f.0) + .is_zero(); } #[test] @@ -395,7 +406,9 @@ fn extracting_ref_f64_is_one() { let value = Float(1.); - assert_that(value).extracting_ref(|f| &f.0).is_one(); + assert_that(value) + .extracting_ref("float.0", |f| &f.0) + .is_one(); } #[test] @@ -404,7 +417,9 @@ fn extracting_ref_f64_is_positive() { let value = Float(1.); - assert_that(value).extracting_ref(|f| &f.0).is_positive(); + assert_that(value) + .extracting_ref("float.0", |f| &f.0) + .is_positive(); } #[test] @@ -413,7 +428,9 @@ fn extracting_ref_f64_is_negative() { let value = Float(-1.); - assert_that(value).extracting_ref(|f| &f.0).is_negative(); + assert_that(value) + .extracting_ref("float.0", |f| &f.0) + .is_negative(); } #[test] @@ -423,7 +440,7 @@ fn extracting_ref_f64_is_not_positive_and_is_not_negative() { let value = Float(0.); assert_that(value) - .extracting_ref(|f| &f.0) + .extracting_ref("float.0", |f| &f.0) .is_not_positive() .is_not_negative(); } @@ -434,7 +451,9 @@ fn extracting_ref_f64_is_infinite() { let value = Float(f64::INFINITY); - assert_that(value).extracting_ref(|f| &f.0).is_infinite(); + assert_that(value) + .extracting_ref("float.0", |f| &f.0) + .is_infinite(); } #[test] @@ -444,7 +463,7 @@ fn extracting_ref_f64_is_not_a_number() { let value = Float(f64::NAN); assert_that(value) - .extracting_ref(|f| &f.0) + .extracting_ref("float.0", |f| &f.0) .is_not_a_number(); } @@ -459,7 +478,9 @@ fn extracting_ref_bigdecimal_has_scale_of() { .unwrap_or_else(|err| panic!("{}", err)), ); - assert_that(number).extracting_ref(|n| &n.0).has_scale_of(8); + assert_that(number) + .extracting_ref("number.0", |n| &n.0) + .has_scale_of(8); } #[cfg(feature = "bigdecimal")] @@ -474,7 +495,7 @@ fn extracting_ref_bigdecimal_has_precision_of() { ); assert_that(number) - .extracting_ref(|n| &n.0) + .extracting_ref("number.0", |n| &n.0) .has_precision_of(12); } @@ -485,21 +506,27 @@ fn extracting_ref_bigdecimal_is_integer() { let number = DecimalNumber("123.0".parse().unwrap_or_else(|err| panic!("{}", err))); - assert_that(number).extracting_ref(|n| &n.0).is_integer(); + assert_that(number) + .extracting_ref("number.0", |n| &n.0) + .is_integer(); } #[test] fn extracting_ref_bool_is_true() { struct Flag(bool); - assert_that(Flag(true)).extracting_ref(|f| &f.0).is_true(); + assert_that(Flag(true)) + .extracting_ref("flag.0", |f| &f.0) + .is_true(); } #[test] fn extracting_ref_bool_is_false() { struct Flag(bool); - assert_that(Flag(false)).extracting_ref(|f| &f.0).is_false(); + assert_that(Flag(false)) + .extracting_ref("flag.0", |f| &f.0) + .is_false(); } #[test] @@ -507,7 +534,7 @@ fn extracting_ref_char_is_lowercase() { struct Character(char); assert_that(Character('r')) - .extracting_ref(|c| &c.0) + .extracting_ref("character.0", |c| &c.0) .is_lowercase(); } @@ -516,7 +543,7 @@ fn extracting_ref_char_is_uppercase() { struct Character(char); assert_that(Character('R')) - .extracting_ref(|c| &c.0) + .extracting_ref("character.0", |c| &c.0) .is_uppercase(); } @@ -525,7 +552,7 @@ fn extracting_ref_char_is_ascii() { struct Character(char); assert_that(Character('@')) - .extracting_ref(|c| &c.0) + .extracting_ref("character.0", |c| &c.0) .is_ascii(); } @@ -533,10 +560,8 @@ fn extracting_ref_char_is_ascii() { fn extracting_ref_char_is_alphabetic() { struct Character(char); - let character = Character('Z'); - - assert_that(character) - .extracting_ref(|c| &c.0) + assert_that(Character('Z')) + .extracting_ref("character.0", |c| &c.0) .is_alphabetic(); } @@ -545,11 +570,11 @@ fn extracting_ref_char_is_alphanumeric() { struct Character(char); assert_that(Character('Z')) - .extracting_ref(|c| &c.0) + .extracting_ref("character.0", |c| &c.0) .is_alphanumeric(); assert_that(Character('5')) - .extracting_ref(|c| &c.0) + .extracting_ref("character.0", |c| &c.0) .is_alphanumeric(); } @@ -558,11 +583,11 @@ fn extracting_ref_char_is_control_char() { struct Character(char); assert_that(Character('\t')) - .extracting_ref(|c| &c.0) + .extracting_ref("character.0", |c| &c.0) .is_control_char(); assert_that(Character('\u{1b}')) - .extracting_ref(|c| &c.0) + .extracting_ref("character.0", |c| &c.0) .is_control_char(); } @@ -571,7 +596,7 @@ fn extracting_ref_char_is_digit() { struct Character(char); assert_that(Character('0')) - .extracting_ref(|c| &c.0) + .extracting_ref("character.0", |c| &c.0) .is_digit(10); } @@ -580,10 +605,10 @@ fn extracting_ref_char_is_whitespace() { struct Character(char); assert_that(Character(' ')) - .extracting_ref(|c| &c.0) + .extracting_ref("character.0", |c| &c.0) .is_whitespace(); assert_that(Character('\n')) - .extracting_ref(|c| &c.0) + .extracting_ref("character.0", |c| &c.0) .is_whitespace(); } @@ -593,7 +618,9 @@ fn extracting_ref_string_is_empty() { let name = Name(String::new()); - assert_that(name).extracting_ref(|n| &n.0).is_empty(); + assert_that(name) + .extracting_ref("name.0", |n| &n.0) + .is_empty(); } #[test] @@ -602,7 +629,9 @@ fn extracting_ref_string_is_not_empty() { let name = Name(" ".to_string()); - assert_that(name).extracting_ref(|n| &n.0).is_not_empty(); + assert_that(name) + .extracting_ref("name.0", |n| &n.0) + .is_not_empty(); } #[test] @@ -611,7 +640,9 @@ fn extracting_ref_vec_is_empty() { let name = Bytes(vec![]); - assert_that(name).extracting_ref(|n| &n.0).is_empty(); + assert_that(name) + .extracting_ref("name.0", |n| &n.0) + .is_empty(); } #[test] @@ -620,7 +651,9 @@ fn extracting_ref_vec_is_not_empty() { let name = Bytes(vec![48, 65]); - assert_that(name).extracting_ref(|n| &n.0).is_not_empty(); + assert_that(name) + .extracting_ref("name.0", |n| &n.0) + .is_not_empty(); } #[test] @@ -629,7 +662,9 @@ fn extracting_ref_string_has_length() { let name = Name("Alex".to_string()); - assert_that(name).extracting_ref(|n| &n.0).has_length(4); + assert_that(name) + .extracting_ref("name.0", |n| &n.0) + .has_length(4); } #[test] @@ -639,7 +674,7 @@ fn extracting_ref_string_has_char_count() { let name = Text("imper \u{0180} diet al \u{02AA} \u{01AF} zzril".to_string()); assert_that(name) - .extracting_ref(|n| &n.0) + .extracting_ref("name.0", |n| &n.0) .has_char_count(25); } @@ -649,7 +684,9 @@ fn extracting_ref_option_some() { let note = Optional(Some("note".to_string())); - assert_that(note).extracting_ref(|n| &n.0).is_some(); + assert_that(note) + .extracting_ref("note.0", |n| &n.0) + .is_some(); } #[test] @@ -658,7 +695,9 @@ fn extracting_ref_option_none() { let note = Optional(None); - assert_that(note).extracting_ref(|n| &n.0).is_none(); + assert_that(note) + .extracting_ref("note.0", |n| &n.0) + .is_none(); } #[test] @@ -668,7 +707,7 @@ fn extracting_ref_option_some_is_equal_to() { let note = Optional(Some("a note".to_string())); assert_that(note) - .extracting_ref(|n| &n.0) + .extracting_ref("note.0", |n| &n.0) .some() .is_equal_to("a note"); } @@ -680,7 +719,7 @@ fn extracting_ref_option_has_value() { let note = Optional(Some("a note".to_string())); assert_that(note) - .extracting_ref(|n| &n.0) + .extracting_ref("note.0", |n| &n.0) .has_value("a note"); } @@ -690,7 +729,9 @@ fn extracting_ref_result_is_ok() { let response = Response(Ok(-123)); - assert_that(response).extracting_ref(|r| &r.0).is_ok(); + assert_that(response) + .extracting_ref("response.0", |r| &r.0) + .is_ok(); } #[test] @@ -699,7 +740,9 @@ fn extracting_ref_result_is_err() { let response = Response(Err("not found".to_string())); - assert_that(response).extracting_ref(|r| &r.0).is_err(); + assert_that(response) + .extracting_ref("response.0", |r| &r.0) + .is_err(); } #[test] @@ -709,7 +752,7 @@ fn extracting_ref_result_ok_is_negative() { let response = Response(Ok(-123)); assert_that(response) - .extracting_ref(|r| &r.0) + .extracting_ref("response.0", |r| &r.0) .ok() .is_negative(); } @@ -721,7 +764,7 @@ fn extracting_ref_result_err_is_equal_to() { let response = Response(Err("not found".to_string())); assert_that(response) - .extracting_ref(|r| &r.0) + .extracting_ref("response.0", |r| &r.0) .err() .is_equal_to("not found"); } @@ -733,7 +776,7 @@ fn extracting_ref_result_has_value() { let response = Response(Ok(-123)); assert_that(response) - .extracting_ref(|r| &r.0) + .extracting_ref("response.0", |r| &r.0) .has_value(-123); } @@ -744,7 +787,7 @@ fn extracting_ref_result_has_error() { let response = Response(Err("not found".to_string())); assert_that(response) - .extracting_ref(|r| &r.0) + .extracting_ref("response.0", |r| &r.0) .has_error("not found"); } @@ -754,7 +797,9 @@ fn extracting_ref_string_contains_char() { let name = Name("Alexander is here".to_string()); - assert_that(name).extracting_ref(|n| &n.0).contains('x'); + assert_that(name) + .extracting_ref("name.0", |n| &n.0) + .contains('x'); } #[test] @@ -764,7 +809,7 @@ fn extracting_ref_string_contains_any_of_chars() { let name = Name("Alexander is here".to_string()); assert_that(name) - .extracting_ref(|n| &n.0) + .extracting_ref("name.0", |n| &n.0) .contains_any_of(['a', 'e', 'i', 'o', 'u']); } @@ -774,7 +819,9 @@ fn extracting_ref_vec_has_length() { let bytes = Bytes(vec![1, 2, 3, 4, 5]); - assert_that(bytes).extracting_ref(|b| &b.0).has_length(5); + assert_that(bytes) + .extracting_ref("bytes.0", |b| &b.0) + .has_length(5); } #[test] @@ -788,7 +835,7 @@ fn extracting_ref_vec_contains() { ]); assert_that(names) - .extracting_ref(|n| &n.0) + .extracting_ref("names.0", |n| &n.0) .contains("Alexander"); } @@ -803,7 +850,7 @@ fn extracting_ref_vec_contains_exactly() { ]); assert_that(names) - .extracting_ref(|n| &n.0) + .extracting_ref("names.0", |n| &n.0) .contains_exactly(["Silvia", "Alexander", "Robert"]); } @@ -817,12 +864,9 @@ fn extracting_ref_vec_contains_only() { "Robert".to_string(), ]); - assert_that(names).extracting_ref(|n| &n.0).contains_only([ - "Silvia", - "Robert", - "Philipp", - "Alexander", - ]); + assert_that(names) + .extracting_ref("names.0", |n| &n.0) + .contains_only(["Silvia", "Robert", "Philipp", "Alexander"]); } #[test] @@ -836,7 +880,7 @@ fn extracting_ref_vec_contains_any() { ]); assert_that(names) - .extracting_ref(|n| &n.0) + .extracting_ref("names.0", |n| &n.0) .contains_any_of(["Robert", "Philipp", "Peter"]); } @@ -851,7 +895,7 @@ fn extracting_ref_vec_contains_all() { ]); assert_that(names) - .extracting_ref(|n| &n.0) + .extracting_ref("names.0", |n| &n.0) .contains_all_of(["Robert", "Silvia"]); } @@ -866,6 +910,6 @@ fn extracting_ref_vec_contains_all_in_order() { ]); assert_that(names) - .extracting_ref(|n| &n.0) + .extracting_ref("names.0", |n| &n.0) .contains_all_in_order(["Silvia", "Robert"]); } diff --git a/src/spec/mod.rs b/src/spec/mod.rs index 891bf42..5195fb8 100644 --- a/src/spec/mod.rs +++ b/src/spec/mod.rs @@ -833,13 +833,17 @@ impl<'a, S, R> Spec<'a, S, R> { } } - pub fn extracting_ref(self, extract: F) -> DerivedSpec<'a, Self, U> + pub fn extracting_ref( + self, + property_name: impl Into>, + extract: F, + ) -> DerivedSpec<'a, Self, U> where F: FnOnce(&S) -> &B, B: ToOwned + ?Sized, { let derived_subject = extract(&self.subject).to_owned(); - let expression = Expression::default(); + let expression = Expression(property_name.into()); let diff_format = self.diff_format.clone(); DerivedSpec::new(self, derived_subject, expression, diff_format) } @@ -1334,23 +1338,19 @@ pub trait And { /// }; /// /// assert_that!(my_friend) - /// .extracting_ref(Person::name) - /// .named("my_friend.name") + /// .extracting_ref("my_friend.name", Person::name) /// .is_equal_to("Silvia") /// .and() - /// .extracting_ref(|p| &p.age) - /// .named("my_friend.age") + /// .extracting_ref("my_friend.age", |p| &p.age) /// .is_at_least(18) /// .and() - /// .extracting_ref(|p| &p.gender) - /// .named("my_friend.gender") + /// .extracting_ref("my_friend.gender", |p| &p.gender) /// .is_equal_to(Gender::Female); /// ``` /// - /// Calling the `named` method after extracting a field is optional but - /// helps in case of a failing assertion as the failure report references - /// the more detailed name, such as `my_friend.name` or `my_friend.age`, - /// instead of just `subject`. + /// The specified property name helps in case of a failing assertion as the + /// failure report references the more detailed name, such as + /// `my_friend.name` or `my_friend.age`, instead of just `subject`. #[must_use = "calling the `and` method without calling another assertion method is useless"] fn and(self) -> Self::Output; } From 9fdf42916404d80d38d5ba6ef9a1e9c94118ee6d Mon Sep 17 00:00:00 2001 From: haraldmaida Date: Mon, 6 Apr 2026 09:03:46 +0200 Subject: [PATCH 12/24] doc: improve phrasing in doc comment of `Spec::extracting` method --- src/spec/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spec/mod.rs b/src/spec/mod.rs index 5195fb8..5c2d10f 100644 --- a/src/spec/mod.rs +++ b/src/spec/mod.rs @@ -789,7 +789,7 @@ impl<'a, S, R> Spec<'a, S, R> { /// This method is similar to the [`mapping()`](Spec::mapping) method. In /// contrast to [`mapping()`](Spec::mapping), this method does not copy the /// subject's name (or expression) but resets it to the default "subject". - /// The idea is that the "extracted" property is definitely a different + /// The idea is that the "extracted" property is most likely a different /// subject than the original one. /// /// It is recommended to give the extracted property a specific name by From 94b18461cb5362486a264f6c71f539b8b2b97e81 Mon Sep 17 00:00:00 2001 From: haraldmaida Date: Sat, 11 Apr 2026 08:29:12 +0200 Subject: [PATCH 13/24] doc: document new method `extracting_ref` and adapt doc comment of `extracting` and `mapping` methods --- src/derived_spec/mod.rs | 244 +++++++++++++++++++++++++++++++++++++++- src/spec/mod.rs | 109 ++++++++++++++---- 2 files changed, 329 insertions(+), 24 deletions(-) diff --git a/src/derived_spec/mod.rs b/src/derived_spec/mod.rs index bb5b543..5091a15 100644 --- a/src/derived_spec/mod.rs +++ b/src/derived_spec/mod.rs @@ -50,12 +50,14 @@ use hashbrown::HashSet; /// A `DerivedSpec` does assertions on a derived subject while keeping track /// of the original subject. /// -/// It has similar functionality as a `Spec`, but additionally holds the +/// It has similar functionality to a [`Spec`], but additionally holds the /// original subject. Calling the `and` method switches the subject back to the /// original subject. /// /// The derived subject can have its own name and diff format in failure /// reports. +/// +/// [`Spec`]: crate::spec::Spec pub struct DerivedSpec<'a, O, S> { original: O, subject: S, @@ -159,6 +161,124 @@ impl And for DerivedSpec<'_, O, S> { } impl<'a, O, S> DerivedSpec<'a, O, S> { + /// Extracts a property from the current subject. + /// + /// The extracting closure gets a reference to the current subject as an + /// argument and should return a reference to the extracted property. The + /// given property name is used in failure reports for referencing the + /// property for which an assertion fails. + /// + /// Use this method if you want to extract multiple properties from the + /// same subject for individual assertions on each of these properties. + /// To extract another property from the previous subject, call the `and` + /// method to switch back to the previous subject before calling + /// `extracting_ref` for the other property. + /// + /// # Arguments + /// + /// * `property_name` - A name describing the extracted property used for + /// referencing that property in failure reports. + /// * `extract` - A closure that returns a reference to the property to be + /// extracted. + /// + /// # Examples + /// + /// ``` + /// use asserting::prelude::*; + /// + /// #[derive(Debug, Clone)] + /// struct Item { + /// name: String, + /// price: f32, + /// quantity: u32, + /// } + /// + /// struct Order { + /// id: String, + /// items: Vec, + /// } + /// + /// let my_order = Order { + /// id: "O261234".into(), + /// items: vec![ + /// Item { + /// name: "Apple".into(), + /// price: 0.99, + /// quantity: 6, + /// }, + /// Item { + /// name: "Orange".into(), + /// price: 1.99, + /// quantity: 4, + /// }, + /// ], + /// }; + /// + /// assert_that!(my_order) + /// .extracting_ref("my_order.items", |o| &o.items) + /// .extracting_ref("my_order.items[0].name", |i| &i[0].name) + /// .is_equal_to("Apple") + /// .and() + /// .extracting_ref("my_order.items[1].name", |i| &i[1].name) + /// .is_equal_to("Orange") + /// .and() + /// .extracting_ref("my_order.items[1].quantity", |i| &i[1].quantity) + /// .is_equal_to(4) + /// .and() // switches back to `my_order.items` + /// .and() // second call to `and()` switches back to `my_order` + /// .extracting_ref("my_order.id", |o| &o.id) + /// .is_equal_to("O261234"); + /// ``` + /// + /// Hint: To avoid having to call the `and()` method two or more times, it + /// is recommended to first extract all properties from the higher level + /// subject and then extract fields from deeper down in the hierarchy. + /// + /// ``` + /// # use asserting::prelude::*; + /// # + /// # #[derive(Debug, Clone)] + /// # struct Item { + /// # name: String, + /// # price: f32, + /// # quantity: u32, + /// # } + /// # + /// # struct Order { + /// # id: String, + /// # items: Vec, + /// # } + /// # + /// # let my_order = Order { + /// # id: "O261234".into(), + /// # items: vec![ + /// # Item { + /// # name: "Apple".into(), + /// # price: 0.99, + /// # quantity: 6, + /// # }, + /// # Item { + /// # name: "Orange".into(), + /// # price: 1.99, + /// # quantity: 4, + /// # }, + /// # ], + /// # }; + /// # + /// assert_that!(my_order) + /// .extracting_ref("my_order.id", |o| &o.id) + /// .is_equal_to("O261234") + /// .and() + /// .extracting_ref("my_order.items", |o| &o.items) + /// .extracting_ref("my_order.items[0].name", |i| &i[0].name) + /// .is_equal_to("Apple") + /// .and() + /// .extracting_ref("my_order.items[1].name", |i| &i[1].name) + /// .is_equal_to("Orange") + /// .and() + /// .extracting_ref("my_order.items[1].quantity", |i| &i[1].quantity) + /// .is_equal_to(4); + /// ``` #[must_use = "a derived spec does nothing unless an assertion method is called"] pub fn extracting_ref( self, @@ -180,6 +300,76 @@ impl<'a, O, S> DerivedSpec<'a, O, S> { } } + /// Maps the current subject to some other value. + /// + /// It takes a closure that maps the current subject to a new subject and + /// returns a new `DerivedSpec` with the value returned by the closure as + /// the new subject. The new subject may have a different type than the + /// original subject. All other data like description, location, and diff + /// format are taken over from this `DerivedSpec` into the returned + /// `DerivedSpec`. + /// + /// This method is useful when having a custom type, and one specific + /// property of this type shall be asserted only. If you want to assert + /// multiple properties of the same subject, use the [`extracting_ref`] + /// method instead. + /// + /// This method is similar to the [`mapping()`](Spec::mapping) method. In + /// contrast to [`mapping()`](Spec::mapping), this method does not copy the + /// subject's name (or expression) but resets it to the default "subject". + /// The idea is that the "extracted" property is most likely a different + /// subject than the original one. + /// + /// It is recommended to give the extracted property a specific name by + /// calling the `named` method. This helps with spotting the cause of a + /// failing assertion. + /// + /// This method does not memorize the current subject. Calling `and` on the + /// extracted property switches back to the original subject of this + /// `DerivedSpec`. The current subject is omitted. So, `and` always switches + /// back to the subject before the last `extracting_ref` call. + /// + /// # Example + /// + /// ``` + /// use asserting::prelude::*; + /// + /// #[derive(Debug, Clone)] + /// struct Item { + /// name: String, + /// price: f32, + /// quantity: u32, + /// } + /// + /// struct Order { + /// id: String, + /// items: Vec, + /// } + /// + /// let my_order = Order { + /// id: "O261234".into(), + /// items: vec![ + /// Item { + /// name: "Apple".into(), + /// price: 0.99, + /// quantity: 6, + /// }, + /// Item { + /// name: "Orange".into(), + /// price: 1.99, + /// quantity: 4, + /// }, + /// ], + /// }; + /// + /// assert_that!(my_order) + /// .extracting_ref("my_order.items", |o| &o.items) + /// .extracting(|i| i[0].name.clone()) + /// .is_equal_to("Apple") + /// .and() // switches back to `my_order` not `my_order.items` + /// .extracting(|o| o.id) + /// .is_equal_to("O261234"); + /// ``` #[must_use = "a derived spec does nothing unless an assertion method is called"] pub fn extracting(self, extract: F) -> DerivedSpec<'a, O, U> where @@ -195,6 +385,58 @@ impl<'a, O, S> DerivedSpec<'a, O, S> { } } + /// Maps the current subject to some other value. + /// + /// It takes a closure that maps the current subject to a new subject and + /// returns a new `DerivedSpec` with the value returned by the closure as + /// the new subject. The new subject may have a different type than the + /// original subject. All other data like expression, description, and + /// location are taken over from this `DerivedSpec` into the returned + /// `DerivedSpec`. + /// + /// This method is useful if some type does not implement a trait required + /// for an assertion. + /// + /// `DerivedSpec` also provides the [`extracting()`](DerivedSpec::extracting) + /// method, which is similar to this method. In contrast to this method, + /// [`extracting()`](DerivedSpec::extracting) does not copy the subject's + /// name (or expression) but resets it to the default "subject". + /// + /// # Example + /// + /// ``` + /// use asserting::prelude::*; + /// + /// #[derive(Clone, Copy)] + /// struct Point { + /// x: i64, + /// y: i64, + /// } + /// + /// struct Line { + /// a: Point, + /// b: Point, + /// } + /// + /// let line = Line { + /// a: Point { x: 12, y: -64 }, + /// b: Point { x: -28, y: 17 }, + /// }; + /// + /// assert_that!(line) + /// .extracting_ref("line.a", |l| &l.a) + /// .mapping(|p| (p.x, p.y)) + /// .is_equal_to((12, -64)) + /// .and() + /// .extracting_ref("line.b", |l| &l.b) + /// .mapping(|p| (p.x, p.y)) + /// .is_equal_to((-28, 17)); + /// ``` + /// + /// The custom type `Point` does not implement the `PartialEq` trait nor + /// the `Debug` trait, which are both required for an `is_equal_to` + /// assertion. So we map the subject of the type `Point` to a tuple of its + /// fields. #[must_use = "a derived spec does nothing unless an assertion method is called"] pub fn mapping(self, map: F) -> DerivedSpec<'a, O, U> where diff --git a/src/spec/mod.rs b/src/spec/mod.rs index 5c2d10f..bb3cd51 100644 --- a/src/spec/mod.rs +++ b/src/spec/mod.rs @@ -775,6 +775,82 @@ impl<'a, S, R> Spec<'a, S, R> { RecursiveComparison::new(self) } + /// Extracts a property from the current subject. + /// + /// The extracting closure gets a reference to the current subject as an + /// argument and should return a reference to the extracted property. The + /// given property name is used in failure reports for referencing the + /// property for which an assertion fails. + /// + /// Use this method if you want to extract multiple properties from the + /// same subject for individual assertions on each of these properties. + /// To extract another property from the original subject, call the `and` + /// method to switch back to the original subject before calling + /// `extracting_ref` for the other property. + /// + /// # Arguments + /// + /// * `property_name` - A name describing the extracted property used for + /// referencing that property in failure reports. + /// * `extract` - A closure that returns a reference to the property to be + /// extracted. + /// + /// # Examples + /// + /// ``` + /// use asserting::prelude::*; + /// + /// #[derive(Debug, Clone, Copy, PartialEq)] + /// enum Gender { + /// Male, + /// Female, + /// NonBinary, + /// PreferNotToSay, + /// } + /// + /// struct Person { + /// name: String, + /// age: u8, + /// gender: Gender, + /// } + /// + /// impl Person { + /// fn name(&self) -> &str { + /// &self.name + /// } + /// } + /// + /// let my_friend = Person { + /// name: "Silvia".into(), + /// age: 27, + /// gender: Gender::Female, + /// }; + /// + /// assert_that!(my_friend) + /// .extracting_ref("my_friend.name", Person::name) + /// .is_equal_to("Silvia") + /// .and() + /// .extracting_ref("my_friend.age", |p| &p.age) + /// .is_at_least(18) + /// .and() + /// .extracting_ref("my_friend.gender", |p| &p.gender) + /// .is_equal_to(Gender::Female); + /// ``` + pub fn extracting_ref( + self, + property_name: impl Into>, + extract: F, + ) -> DerivedSpec<'a, Self, U> + where + F: FnOnce(&S) -> &B, + B: ToOwned + ?Sized, + { + let derived_subject = extract(&self.subject).to_owned(); + let expression = Expression(property_name.into()); + let diff_format = self.diff_format.clone(); + DerivedSpec::new(self, derived_subject, expression, diff_format) + } + /// Maps the current subject to some other value. /// /// It takes a closure that maps the current subject to a new subject and @@ -783,8 +859,10 @@ impl<'a, S, R> Spec<'a, S, R> { /// subject. All other data like description, location, and diff format are /// taken over from this `Spec` into the returned `Spec`. /// - /// This function is useful when having a custom type, and a specific - /// property of this type shall be asserted only. + /// This method is useful when having a custom type, and one specific + /// property of this type shall be asserted only. If you want to assert + /// multiple properties of the same subject, use the [`extracting_ref`] + /// method instead. /// /// This method is similar to the [`mapping()`](Spec::mapping) method. In /// contrast to [`mapping()`](Spec::mapping), this method does not copy the @@ -833,21 +911,6 @@ impl<'a, S, R> Spec<'a, S, R> { } } - pub fn extracting_ref( - self, - property_name: impl Into>, - extract: F, - ) -> DerivedSpec<'a, Self, U> - where - F: FnOnce(&S) -> &B, - B: ToOwned + ?Sized, - { - let derived_subject = extract(&self.subject).to_owned(); - let expression = Expression(property_name.into()); - let diff_format = self.diff_format.clone(); - DerivedSpec::new(self, derived_subject, expression, diff_format) - } - /// Maps the current subject to some other value. /// /// It takes a closure that maps the current subject to a new subject and @@ -856,13 +919,13 @@ impl<'a, S, R> Spec<'a, S, R> { /// subject. All other data like expression, description, and location are /// taken over from this `Spec` into the returned `Spec`. /// - /// This function is useful if some type does not implement a trait - /// required for an assertion. + /// This method is useful if some type does not implement a trait required + /// for an assertion. /// - /// `Spec` also provides the [`extracting()`](Spec::extracting) function, - /// which is an alias to this function. Both functions do exactly the same. - /// Choose that function of which its name expresses the intent more - /// clearly. + /// `Spec` also provides the [`extracting()`](Spec::extracting) method, + /// which is similar to this method. In contrast to this method, + /// [`extracting()`](Spec::extracting) does not copy the subject's name + /// (or expression) but resets it to the default "subject". /// /// # Example /// From 25829b4a7efc1eb0a01171754658b999fd066795 Mon Sep 17 00:00:00 2001 From: haraldmaida Date: Sat, 18 Apr 2026 17:19:18 +0200 Subject: [PATCH 14/24] feat: define trait `AssertOrderedElementsRef` and implement it for `Spec` --- src/assertions.rs | 164 +++++++- src/iterator/mod.rs | 95 ++++- src/iterator/tests.rs | 513 +++++++++++++++++++++----- src/recursive_comparison/value/mod.rs | 2 +- src/spec/mod.rs | 31 +- 5 files changed, 684 insertions(+), 121 deletions(-) diff --git a/src/assertions.rs b/src/assertions.rs index 93115e4..2018e64 100644 --- a/src/assertions.rs +++ b/src/assertions.rs @@ -4107,25 +4107,27 @@ pub trait AssertElements { P: FnMut(&T) -> bool; } -/// Filter assertions for elements of a collection or an iterator that yields -/// its elements in a defined order. +/// Extract one or multiple elements of a collection or an iterator that yields +/// its elements in a defined order to call assertions on the extracted +/// elements. /// -/// Filtering is used to target the assertions on specific elements of a +/// Extraction is used to target the assertions on specific elements of a /// collection or an iterator, such as the first or last element. /// +/// See also the [`AssertOrderedElementsRef`] trait for extracting multiple +/// elements from the same ordered collection or iterator for individual +/// assertions. +/// /// # Examples /// /// ``` /// use asserting::prelude::*; /// -/// let subject = ["first", "second", "third", "four", "five"]; +/// let subject = ["one", "two", "three", "four", "five"]; /// -/// assert_that!(subject).first_element().is_equal_to("first"); +/// assert_that!(subject).first_element().is_equal_to("one"); /// assert_that!(subject).last_element().is_equal_to("five"); /// assert_that!(subject).nth_element(3).is_equal_to("four"); -/// -/// let subject = ["one", "two", "three", "four", "five"]; -/// /// assert_that!(subject) /// .elements_at([0, 2, 4]) /// .contains_exactly(["one", "three", "five"]); @@ -4150,9 +4152,9 @@ pub trait AssertOrderedElements { /// ``` /// use asserting::prelude::*; /// - /// let subject = ["first", "second", "third"]; + /// let subject = ["one", "two", "three"]; /// - /// assert_that!(subject).first_element().is_equal_to("first"); + /// assert_that!(subject).first_element().is_equal_to("one"); /// ``` /// /// [`Spec`]: crate::spec::Spec @@ -4167,9 +4169,9 @@ pub trait AssertOrderedElements { /// ``` /// use asserting::prelude::*; /// - /// let subject = ["first", "second", "third"]; + /// let subject = ["one", "two", "three"]; /// - /// assert_that!(subject).last_element().is_equal_to("third"); + /// assert_that!(subject).last_element().is_equal_to("three"); /// ``` /// /// [`Spec`]: crate::spec::Spec @@ -4186,11 +4188,11 @@ pub trait AssertOrderedElements { /// ``` /// use asserting::prelude::*; /// - /// let subject = ["first", "second", "third"]; + /// let subject = ["one", "two", "three"]; /// - /// assert_that!(subject).nth_element(0).is_equal_to("first"); - /// assert_that!(subject).nth_element(1).is_equal_to("second"); - /// assert_that!(subject).nth_element(2).is_equal_to("third"); + /// assert_that!(subject).nth_element(0).is_equal_to("one"); + /// assert_that!(subject).nth_element(1).is_equal_to("two"); + /// assert_that!(subject).nth_element(2).is_equal_to("three"); /// ``` /// /// [`Spec`]: crate::spec::Spec @@ -4216,3 +4218,133 @@ pub trait AssertOrderedElements { #[track_caller] fn elements_at(self, indices: impl IntoIterator) -> Self::MultipleElements; } + +/// Extract one or multiple elements of a collection or an iterator that yields +/// its elements in a defined order to call assertions on the extracted +/// elements. +/// +/// Extraction is used to target the assertions on specific elements of a +/// collection or an iterator, such as the first or last element. +/// +/// The methods of this trait can be used in combination with the `and` method +/// to extract multiple elements from a collection for individual assertions. +/// The downside is that the elements of the collection or iterator must +/// implement the `ToOwned` trait. +/// +/// If you only want to call a single extraction method on the same collection +/// or iterator, you can use the methods of the [`AssertOrderedElements`] trait. +/// These methods do not require that the elements of the collection implement +/// `ToOwned` or `Clone`. +/// +/// # Examples +/// +/// ``` +/// use asserting::prelude::*; +/// +/// let subject = ["one", "two", "three", "four", "five"]; +/// +/// assert_that!(subject) +/// .first_element_ref().is_equal_to("one") +/// .and() +/// .last_element_ref().is_equal_to("five") +/// .and() +/// .nth_element_ref(3).is_equal_to("four") +/// .and() +/// .elements_ref_at([0, 2, 4]).contains_exactly(["one", "three", "five"]); +/// ``` +pub trait AssertOrderedElementsRef { + /// A spec-like type that contains a single element as the subject that is + /// extracted from the iterator. + /// + /// Usually this is a `Spec<'a, T, R>`. + type SingleElement; + /// A spec-like type that contains multiple or all elements of an iterator + /// as the subject. + /// + /// Usually this is a `Spec<'a, Vec, R>`. + type MultipleElements; + + /// Verify that a collection or an iterator contains at least one element + /// and return a [`Spec`] for the first element. + /// + /// # Examples + /// + /// ``` + /// use asserting::prelude::*; + /// + /// let subject = ["one", "two", "three"]; + /// + /// assert_that!(subject) + /// .first_element_ref().is_equal_to("one") + /// .and() + /// .last_element_ref().is_equal_to("three"); + /// ``` + /// + /// [`Spec`]: crate::spec::Spec + #[track_caller] + fn first_element_ref(self) -> Self::SingleElement; + + /// Verify that a collection or an iterator contains at least one element + /// and return a [`Spec`] for the last element. + /// + /// # Examples + /// + /// ``` + /// use asserting::prelude::*; + /// + /// let subject = ["one", "two", "three"]; + /// + /// assert_that!(subject) + /// .last_element_ref().is_equal_to("three") + /// .and() + /// .first_element_ref().is_equal_to("one"); + /// ``` + /// + /// [`Spec`]: crate::spec::Spec + #[track_caller] + fn last_element_ref(self) -> Self::SingleElement; + + /// Verify that a collection or an iterator contains at least n + 1 elements + /// and return a [`Spec`] for the nth element. + /// + /// The index n is zero-based (similar to the `nth` method of iterators). + /// + /// # Examples + /// + /// ``` + /// use asserting::prelude::*; + /// + /// let subject = ["one", "two", "three"]; + /// + /// assert_that!(subject) + /// .nth_element_ref(0).is_equal_to("one") + /// .and() + /// .nth_element_ref(1).is_equal_to("two") + /// .and() + /// .nth_element_ref(2).is_equal_to("three"); + /// ``` + /// + /// [`Spec`]: crate::spec::Spec + #[track_caller] + fn nth_element_ref(self, n: usize) -> Self::SingleElement; + + /// Pick the elements of a collection or an iterator at the given positions + /// and return a [`Spec`] only containing the selected elements. + /// + /// # Examples + /// + /// ``` + /// use asserting::prelude::*; + /// + /// let subject = ["one", "two", "three", "four", "five"]; + /// + /// assert_that!(subject) + /// .elements_ref_at([0, 2, 4]).contains_exactly(["one", "three", "five"]) + /// .and() + /// .elements_ref_at([1, 3]).contains_exactly(["two", "four"]); + /// ``` + /// + /// [`Spec`]: crate::spec::Spec + #[track_caller] + fn elements_ref_at(self, indices: impl IntoIterator) -> Self::MultipleElements; +} diff --git a/src/iterator/mod.rs b/src/iterator/mod.rs index 165dca4..ac0ac1c 100644 --- a/src/iterator/mod.rs +++ b/src/iterator/mod.rs @@ -2,12 +2,13 @@ use crate::assertions::{ AssertElements, AssertIteratorContains, AssertIteratorContainsInAnyOrder, - AssertIteratorContainsInOrder, AssertOrderedElements, + AssertIteratorContainsInOrder, AssertOrderedElements, AssertOrderedElementsRef, }; use crate::colored::{ mark_all_items_in_collection, mark_missing, mark_missing_string, mark_selected_items_in_collection, mark_unexpected, mark_unexpected_string, }; +use crate::derived_spec::DerivedSpec; use crate::expectations::{ AllSatisfy, AnySatisfies, HasAtLeastNumberOfElements, HasSingleElement, IteratorContains, IteratorContainsAllInOrder, IteratorContainsAllOf, IteratorContainsAnyOf, @@ -24,6 +25,7 @@ use crate::spec::{ DiffFormat, Expectation, Expecting, Expression, FailingStrategy, GetFailures, Invertible, PanicOnFail, Spec, }; +use crate::std::borrow::ToOwned; use crate::std::cmp::Ordering; use crate::std::fmt::Debug; use crate::std::mem; @@ -1004,6 +1006,9 @@ where } fn elements_at(self, indices: impl IntoIterator) -> Self::MultipleElements { + let indices = Vec::from_iter(indices); + let orig_subject_name = self.expression(); + let new_subject_name = format!("{orig_subject_name} at positions {indices:?}"); let indices = HashSet::<_>::from_iter(indices); self.mapping(|subject| { subject @@ -1012,6 +1017,94 @@ where .filter_map(|(i, v)| if indices.contains(&i) { Some(v) } else { None }) .collect() }) + .named(new_subject_name) + } +} + +impl<'a, S, T, U, R> AssertOrderedElementsRef for Spec<'a, S, R> +where + S: IntoIterator, + ::IntoIter: DefinedOrderProperty, + T: 'a + ToOwned + Debug, + U: Clone, + R: FailingStrategy, +{ + type SingleElement = DerivedSpec<'a, Spec<'a, Vec, R>, U>; + type MultipleElements = DerivedSpec<'a, Spec<'a, Vec, R>, Vec>; + + fn first_element_ref(self) -> Self::SingleElement { + let original_spec = self + .mapping(Vec::from_iter) + .expecting(has_at_least_number_of_elements(1)); + if original_spec.has_failures() { + PanicOnFail.do_fail_with(&original_spec.failures()); + unreachable!("Assertion failed and should have panicked! Please report a bug.") + } + let orig_subject_name = original_spec.expression(); + let new_subject_name = format!("the first element of {orig_subject_name}"); + original_spec.extracting_ref(new_subject_name, |collection| + collection.first() + .unwrap_or_else(|| + unreachable!("We should have asserted before, that there is at least one element in the collection/iterator. Please file a bug.") + ) + ) + } + + fn last_element_ref(self) -> Self::SingleElement { + let original_spec = self + .mapping(Vec::from_iter) + .expecting(has_at_least_number_of_elements(1)); + if original_spec.has_failures() { + PanicOnFail.do_fail_with(&original_spec.failures()); + unreachable!("Assertion failed and should have panicked! Please report a bug.") + } + let orig_subject_name = original_spec.expression(); + let new_subject_name = format!("the last element of {orig_subject_name}"); + original_spec.extracting_ref(new_subject_name, |collection| + collection.last() + .unwrap_or_else(|| + unreachable!("We should have asserted before, that there is at least one element in the collection/iterator. Please file a bug.") + ) + ) + } + + fn nth_element_ref(self, n: usize) -> Self::SingleElement { + let min_len = n + 1; + let original_spec = self + .mapping(Vec::from_iter) + .expecting(has_at_least_number_of_elements(min_len)); + if original_spec.has_failures() { + PanicOnFail.do_fail_with(&original_spec.failures()); + unreachable!("Assertion failed and should have panicked! Please report a bug.") + } + let orig_subject_name = original_spec.expression(); + let new_subject_name = format!("{orig_subject_name}[{n}]"); + original_spec.extracting_ref(new_subject_name, |collection| + collection.get(n) + .unwrap_or_else(|| + unreachable!("We should have asserted before, that there is at least one element in the collection/iterator. Please file a bug.") + ) + ) + } + + fn elements_ref_at(self, indices: impl IntoIterator) -> Self::MultipleElements { + let indices = Vec::from_iter(indices); + let orig_subject_name = self.expression(); + let new_subject_name = format!("{orig_subject_name} at positions {indices:?}"); + let indices = HashSet::<_>::from_iter(indices); + let original_spec = self.mapping(Vec::from_iter); + original_spec.extracting_ref_iter(new_subject_name, |collection| { + collection + .enumerate() + .filter_map(|(i, e)| { + if indices.contains(&i) { + Some(e.to_owned()) + } else { + None + } + }) + .collect() + }) } } diff --git a/src/iterator/tests.rs b/src/iterator/tests.rs index 8256dc1..fdb6457 100644 --- a/src/iterator/tests.rs +++ b/src/iterator/tests.rs @@ -231,7 +231,7 @@ fn verify_custom_iterator_does_not_contain_fails() { ); } -mod element_filters { +mod filtered_elements { use super::*; #[test] @@ -287,6 +287,173 @@ mod element_filters { ); } + #[test] + fn filtered_on_elements_of_iterator_even_elements() { + let subject = CustomCollection { + inner: vec![1, 2, 3, 4, 5], + }; + + assert_that(subject) + .filtered_on(|e| e & 1 == 0) + .contains_exactly_in_any_order([2, 4]); + } + + #[test] + fn elements_at_positions_of_iterator() { + let subject = CustomOrderedCollection { + inner: vec!["one", "two", "three", "four", "five"], + }; + + assert_that(subject) + .elements_at([0, 2, 4]) + .contains_exactly(["one", "three", "five"]); + } + + #[test] + fn any_satisfies_on_elements_of_iterator_value_is_equal_to_42() { + let subject = CustomCollection { + inner: vec![1, 41, 43, 42, 5], + }; + + assert_that(subject).any_satisfies(|e| *e == 42); + } + + #[test] + fn verify_any_satisfies_on_elements_of_iterator_value_is_equal_to_42_fails() { + let subject = CustomCollection { + inner: vec![1, 2, 43, 41, 5], + }; + + let failures = verify_that(subject) + .named("my_numbers") + .any_satisfies(|e| *e == 42) + .display_failures(); + + assert_eq!( + failures, + &[ + r"expected any element of my_numbers to satisfy the predicate, but none did + actual: [1, 2, 43, 41, 5] +" + ] + ); + } + + #[test] + fn all_satisfy_on_elements_of_iterator_value_is_greater_than_42() { + let subject = CustomCollection { + inner: vec![47, 46, 45, 44, 43], + }; + + assert_that(subject).all_satisfy(|e| *e > 42); + } + + #[test] + fn verify_all_satisfy_on_elements_of_iterator_value_is_greater_than_42_fails() { + let subject = CustomCollection { + inner: vec![43, 44, 45, 42, 47], + }; + + let failures = verify_that(subject) + .named("my_numbers") + .all_satisfy(|e| *e > 42) + .display_failures(); + + assert_eq!( + failures, + &[ + r"expected all elements of my_numbers to satisfy the predicate, but 1 did not + actual: [43, 44, 45, 42, 47] + failing: [42] +" + ] + ); + } + + #[test] + fn none_satisfies_on_elements_of_iterator_value_is_greater_than_42() { + let subject = CustomCollection { + inner: vec![42, 41, 40, 39, 38], + }; + + assert_that(subject).none_satisfies(|e| *e > 42); + } + + #[test] + fn verify_none_satisfies_on_elements_of_iterator_value_is_greater_than_42_fails() { + let subject = CustomCollection { + inner: vec![41, 43, 45, 42, 47], + }; + + let failures = verify_that(subject) + .named("my_numbers") + .none_satisfies(|e| *e > 42) + .display_failures(); + + assert_eq!( + failures, + &[ + r"expected none of the elements of my_numbers to satisfy the predicate, but 3 did + actual: [41, 43, 45, 42, 47] + failing: [43, 45, 47] +" + ] + ); + } + + #[cfg(feature = "colored")] + mod colored { + use super::*; + + #[test] + fn highlight_all_satisfy_on_elements_of_iterator() { + let subject = CustomCollection { + inner: vec![43, 44, 45, 42, 47], + }; + + let failures = verify_that(subject) + .named("my_numbers") + .with_diff_format(DIFF_FORMAT_RED_YELLOW) + .all_satisfy(|e| *e > 42) + .display_failures(); + + assert_eq!( + failures, + &[ + "expected all elements of my_numbers to satisfy the predicate, but 1 did not\n \ + actual: [43, 44, 45, \u{1b}[31m42\u{1b}[0m, 47]\n \ + failing: [42]\n" + ] + ); + } + + #[test] + fn highlight_none_satisfies_on_elements_of_iterator() { + let subject = CustomCollection { + inner: vec![41, 43, 45, 42, 47], + }; + + let failures = verify_that(subject) + .named("my_numbers") + .with_diff_format(DIFF_FORMAT_RED_YELLOW) + .none_satisfies(|e| *e > 42) + .display_failures(); + + assert_eq!( + failures, + &[ + "expected none of the elements of my_numbers to satisfy the predicate, but 3 did\n \ + actual: [41, \u{1b}[31m43\u{1b}[0m, \u{1b}[31m45\u{1b}[0m, 42, \u{1b}[31m47\u{1b}[0m]\n \ + failing: [43, 45, 47]\n" + ] + ); + } + } +} + +mod extracted_elements { + use super::*; + #[test] fn first_element_of_iterator_with_one_element() { let subject = CustomOrderedCollection { @@ -437,17 +604,6 @@ mod element_filters { ); } - #[test] - fn filtered_on_elements_of_iterator_even_elements() { - let subject = CustomCollection { - inner: vec![1, 2, 3, 4, 5], - }; - - assert_that(subject) - .filtered_on(|e| e & 1 == 0) - .contains_exactly_in_any_order([2, 4]); - } - #[test] fn elements_at_positions_of_iterator() { let subject = CustomOrderedCollection { @@ -460,143 +616,308 @@ mod element_filters { } #[test] - fn any_satisfies_on_elements_of_iterator_value_is_equal_to_42() { - let subject = CustomCollection { - inner: vec![1, 41, 43, 42, 5], - }; + fn verify_elements_at_positions_of_empty_iterator_fails() { + let subject: CustomOrderedCollection<&str> = CustomOrderedCollection { inner: vec![] }; - assert_that(subject).any_satisfies(|e| *e == 42); + let failures = verify_that(subject) + .named("my_custom_collection") + .elements_at([0, 1]) + .contains_exactly(["one", "two"]) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected my_custom_collection at positions [0, 1] to contain exactly in order ["one", "two"] + but was: [] + expected: ["one", "two"] + missing: ["one", "two"] + extra: [] + out-of-order: [] +"# + ] + ); } #[test] - fn verify_any_satisfies_on_elements_of_iterator_value_is_equal_to_42_fails() { - let subject = CustomCollection { - inner: vec![1, 2, 43, 41, 5], + fn verify_elements_at_out_of_bounds_position_fails() { + let subject = CustomOrderedCollection { + inner: vec!["one", "two", "three"], }; let failures = verify_that(subject) - .named("my_numbers") - .any_satisfies(|e| *e == 42) + .named("my_custom_collection") + .elements_at([0, 3]) + .contains_exactly(["one", "four"]) .display_failures(); assert_eq!( failures, &[ - r"expected any element of my_numbers to satisfy the predicate, but none did - actual: [1, 2, 43, 41, 5] -" + r#"expected my_custom_collection at positions [0, 3] to contain exactly in order ["one", "four"] + but was: ["one"] + expected: ["one", "four"] + missing: ["four"] + extra: [] + out-of-order: [] +"# ] ); } +} + +mod iterator_extracted_elements_ref { + use super::*; #[test] - fn all_satisfy_on_elements_of_iterator_value_is_greater_than_42() { - let subject = CustomCollection { - inner: vec![47, 46, 45, 44, 43], - }; + fn first_element_of_iterator_with_one_element() { + let subject = vec!["single"]; - assert_that(subject).all_satisfy(|e| *e > 42); + assert_that(subject) + .first_element_ref() + .is_equal_to("single") + .has_length(6) + .starts_with("si"); } #[test] - fn verify_all_satisfy_on_elements_of_iterator_value_is_greater_than_42_fails() { - let subject = CustomCollection { - inner: vec![43, 44, 45, 42, 47], - }; + fn first_element_of_iterator_with_several_elements() { + let subject = vec!["one", "two", "three", "four", "five"]; + + assert_that(subject) + .first_element_ref() + .is_equal_to("one") + .has_length(3) + .starts_with('o'); + } + + #[cfg(feature = "panic")] + #[test] + fn first_element_of_iterator_with_no_elements_fails() { + let subject: Vec = vec![]; + + assert_that_code(|| { + assert_that(subject) + .named("my_custom_collection") + .with_diff_format(DIFF_FORMAT_NO_HIGHLIGHT) + .first_element_ref() + .is_equal_to(42); + }) + .panics_with_message( + r"expected my_custom_collection to have at least one element, but has no elements + actual: [] +", + ); + } + + #[test] + fn verfiy_first_element_of_iterator_assertion_fails() { + let subject = vec!["four", "two", "three"]; let failures = verify_that(subject) - .named("my_numbers") - .all_satisfy(|e| *e > 42) + .named("my_collection") + .first_element_ref() + .is_equal_to("one") .display_failures(); assert_eq!( failures, &[ - r"expected all elements of my_numbers to satisfy the predicate, but 1 did not - actual: [43, 44, 45, 42, 47] - failing: [42] -" + r#"expected the first element of my_collection to be equal to "one" + but was: "four" + expected: "one" +"# ] ); } #[test] - fn none_satisfies_on_elements_of_iterator_value_is_greater_than_42() { - let subject = CustomCollection { - inner: vec![42, 41, 40, 39, 38], - }; + fn last_element_of_iterator_with_one_element() { + let subject = vec!["single"]; - assert_that(subject).none_satisfies(|e| *e > 42); + assert_that(subject) + .last_element_ref() + .is_equal_to("single") + .has_length(6) + .starts_with("si"); } #[test] - fn verify_none_satisfies_on_elements_of_iterator_value_is_greater_than_42_fails() { - let subject = CustomCollection { - inner: vec![41, 43, 45, 42, 47], - }; + fn last_element_of_iterator_with_several_elements() { + let subject = vec!["one", "two", "three", "four", "five"]; + + assert_that(subject) + .last_element_ref() + .is_equal_to("five") + .has_length(4) + .starts_with("fi"); + } + + #[cfg(feature = "panic")] + #[test] + fn last_element_of_iterator_with_no_elements_fails() { + let subject: Vec = vec![]; + + assert_that_code(|| { + assert_that(subject) + .named("my_custom_collection") + .with_diff_format(DIFF_FORMAT_NO_HIGHLIGHT) + .last_element_ref() + .is_equal_to(42); + }) + .panics_with_message( + r"expected my_custom_collection to have at least one element, but has no elements + actual: [] +", + ); + } + + #[test] + fn verfiy_last_element_of_iterator_assertion_fails() { + let subject = vec!["one", "two", "four"]; let failures = verify_that(subject) - .named("my_numbers") - .none_satisfies(|e| *e > 42) + .named("my_collection") + .last_element_ref() + .is_equal_to("three") .display_failures(); assert_eq!( failures, &[ - r"expected none of the elements of my_numbers to satisfy the predicate, but 3 did - actual: [41, 43, 45, 42, 47] - failing: [43, 45, 47] -" + r#"expected the last element of my_collection to be equal to "three" + but was: "four" + expected: "three" +"# ] ); } - #[cfg(feature = "colored")] - mod colored { - use super::*; + #[test] + fn nth_element_of_iterator_with_one_element() { + let subject = vec!["single"]; - #[test] - fn highlight_all_satisfy_on_elements_of_iterator() { - let subject = CustomCollection { - inner: vec![43, 44, 45, 42, 47], - }; + assert_that(subject) + .nth_element_ref(0) + .is_equal_to("single") + .has_length(6) + .starts_with("si"); + } - let failures = verify_that(subject) - .named("my_numbers") - .with_diff_format(DIFF_FORMAT_RED_YELLOW) - .all_satisfy(|e| *e > 42) - .display_failures(); + #[test] + fn nth_element_of_iterator_with_several_elements_second_element() { + let subject = vec!["one", "two", "three", "four", "five"]; - assert_eq!( - failures, - &[ - "expected all elements of my_numbers to satisfy the predicate, but 1 did not\n \ - actual: [43, 44, 45, \u{1b}[31m42\u{1b}[0m, 47]\n \ - failing: [42]\n" - ] - ); - } + assert_that(subject) + .nth_element_ref(1) + .is_equal_to("two") + .has_length(3) + .starts_with("tw"); + } - #[test] - fn highlight_none_satisfies_on_elements_of_iterator() { - let subject = CustomCollection { - inner: vec![41, 43, 45, 42, 47], - }; + #[test] + fn nth_element_of_iterator_with_several_elements_fifth_element() { + let subject = vec!["one", "two", "three", "four", "five"]; - let failures = verify_that(subject) - .named("my_numbers") - .with_diff_format(DIFF_FORMAT_RED_YELLOW) - .none_satisfies(|e| *e > 42) - .display_failures(); + assert_that(subject) + .nth_element_ref(4) + .is_equal_to("five") + .has_length(4) + .starts_with("fi"); + } - assert_eq!( - failures, - &[ - "expected none of the elements of my_numbers to satisfy the predicate, but 3 did\n \ - actual: [41, \u{1b}[31m43\u{1b}[0m, \u{1b}[31m45\u{1b}[0m, 42, \u{1b}[31m47\u{1b}[0m]\n \ - failing: [43, 45, 47]\n" - ] - ); - } + #[cfg(feature = "panic")] + #[test] + fn nth_element_of_iterator_with_five_elements_6th_element_fails() { + let subject = vec!["one", "two", "three", "four", "five"]; + + assert_that_code(|| { + assert_that(subject) + .named("my_custom_collection") + .with_diff_format(DIFF_FORMAT_NO_HIGHLIGHT) + .nth_element_ref(5) + .is_equal_to("five"); + }) + .panics_with_message( + r#"expected my_custom_collection to have at least 6 elements, but has 5 elements + actual: ["one", "two", "three", "four", "five"] +"#, + ); + } + + #[test] + fn verfiy_nth_element_of_iterator_assertion_fails() { + let subject = vec!["one", "four", "three"]; + + let failures = verify_that(subject) + .named("my_collection") + .nth_element_ref(1) + .is_equal_to("two") + .display_failures(); + + assert_eq!( + failures, + &[r#"expected my_collection[1] to be equal to "two" + but was: "four" + expected: "two" +"#] + ); + } + + #[test] + fn elements_at_positions_of_iterator() { + let subject = vec!["one", "two", "three", "four", "five"]; + + assert_that(subject) + .elements_ref_at([0, 2, 4]) + .contains_exactly(["one", "three", "five"]); + } + + #[test] + fn verify_elements_at_positions_of_empty_iterator_fails() { + let subject: Vec<&str> = vec![]; + + let failures = verify_that(subject) + .named("my_custom_collection") + .elements_ref_at([0, 1]) + .contains_exactly(["one", "two"]) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected my_custom_collection at positions [0, 1] to contain exactly in order ["one", "two"] + but was: [] + expected: ["one", "two"] + missing: ["one", "two"] + extra: [] + out-of-order: [] +"# + ] + ); + } + + #[test] + fn verify_elements_at_out_of_bounds_position_fails() { + let subject = vec!["one", "two", "three"]; + + let failures = verify_that(subject) + .named("my_custom_collection") + .elements_ref_at([0, 3]) + .contains_exactly(["one", "four"]) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected my_custom_collection at positions [0, 3] to contain exactly in order ["one", "four"] + but was: ["one"] + expected: ["one", "four"] + missing: ["four"] + extra: [] + out-of-order: [] +"# + ] + ); } } diff --git a/src/recursive_comparison/value/mod.rs b/src/recursive_comparison/value/mod.rs index aca61f1..249be4a 100644 --- a/src/recursive_comparison/value/mod.rs +++ b/src/recursive_comparison/value/mod.rs @@ -28,7 +28,7 @@ impl Debug for Field { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let name = &self.name; let value = &self.value; - write!(f, "{name}: {value:?}",) + write!(f, "{name}: {value:?}") } } diff --git a/src/spec/mod.rs b/src/spec/mod.rs index bb3cd51..5bed173 100644 --- a/src/spec/mod.rs +++ b/src/spec/mod.rs @@ -11,6 +11,7 @@ use crate::std::error::Error as StdError; use crate::std::fmt::{self, Debug, Display}; use crate::std::format; use crate::std::ops::Deref; +use crate::std::slice; use crate::std::string::{String, ToString}; use crate::std::vec; use crate::std::vec::Vec; @@ -1050,7 +1051,10 @@ where } } -impl<'a, I, R> Spec<'a, I, R> { +impl<'a, I, R> Spec<'a, I, R> +where + I: IntoIterator, +{ /// Iterates over the elements of a collection or an iterator and executes /// the given assertions for each of those elements. If all elements are /// asserted successfully, the whole assertion succeeds. @@ -1096,10 +1100,9 @@ impl<'a, I, R> Spec<'a, I, R> { /// ``` #[allow(clippy::return_self_not_must_use)] #[track_caller] - pub fn each_element(mut self, assert: A) -> Spec<'a, (), R> + pub fn each_element(mut self, assert: A) -> Spec<'a, (), R> where - I: IntoIterator, - A: Fn(Spec<'a, T, CollectFailures>) -> Spec<'a, B, CollectFailures>, + A: Fn(Spec<'a, ::Item, CollectFailures>) -> Spec<'a, B, CollectFailures>, { let root_expression = &self.expression; let mut position = -1; @@ -1175,10 +1178,9 @@ impl<'a, I, R> Spec<'a, I, R> { /// expected: 'x' /// ``` #[track_caller] - pub fn any_element(mut self, assert: A) -> Spec<'a, (), R> + pub fn any_element(mut self, assert: A) -> Spec<'a, (), R> where - I: IntoIterator, - A: Fn(Spec<'a, T, CollectFailures>) -> Spec<'a, B, CollectFailures>, + A: Fn(Spec<'a, ::Item, CollectFailures>) -> Spec<'a, B, CollectFailures>, { let root_expression = &self.expression; let mut any_success = false; @@ -1216,6 +1218,21 @@ impl<'a, I, R> Spec<'a, I, R> { failing_strategy: self.failing_strategy, } } + + pub(crate) fn extracting_ref_iter( + self, + property_name: impl Into>, + extract: F, + ) -> DerivedSpec<'a, Spec<'a, Vec<::Item>, R>, Vec> + where + for<'b> F: Fn(slice::Iter<'b, ::Item>) -> Vec, + { + let property_name = Expression(property_name.into()); + let diff_format = self.diff_format.clone(); + let orig_spec = self.mapping(Vec::from_iter); + let new_subject = extract(orig_spec.subject.iter()); + DerivedSpec::new(orig_spec, new_subject, property_name, diff_format) + } } /// Trigger failing of an assertion according to the failing strategy of its From bf3093d9cc2b1b6156e6d6e44abef101e1268978 Mon Sep 17 00:00:00 2001 From: haraldmaida Date: Sat, 18 Apr 2026 17:31:25 +0200 Subject: [PATCH 15/24] doc: fix broken links in doc comments --- .../jetbrains/runConfigurations/Doc.run.xml | 19 +++++++++++++++++++ src/derived_spec/mod.rs | 13 ++++++++----- src/lib.rs | 2 +- src/spec/mod.rs | 13 ++++++++----- 4 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 .ide-settings/jetbrains/runConfigurations/Doc.run.xml diff --git a/.ide-settings/jetbrains/runConfigurations/Doc.run.xml b/.ide-settings/jetbrains/runConfigurations/Doc.run.xml new file mode 100644 index 0000000..6a9cbbf --- /dev/null +++ b/.ide-settings/jetbrains/runConfigurations/Doc.run.xml @@ -0,0 +1,19 @@ + + + + \ No newline at end of file diff --git a/src/derived_spec/mod.rs b/src/derived_spec/mod.rs index 5091a15..f0005de 100644 --- a/src/derived_spec/mod.rs +++ b/src/derived_spec/mod.rs @@ -314,11 +314,11 @@ impl<'a, O, S> DerivedSpec<'a, O, S> { /// multiple properties of the same subject, use the [`extracting_ref`] /// method instead. /// - /// This method is similar to the [`mapping()`](Spec::mapping) method. In - /// contrast to [`mapping()`](Spec::mapping), this method does not copy the - /// subject's name (or expression) but resets it to the default "subject". - /// The idea is that the "extracted" property is most likely a different - /// subject than the original one. + /// This method is similar to the [`mapping`] method. In contrast to + /// [`mapping`], this method does not copy the subject's name + /// (or expression) but resets it to the default "subject". The idea is + /// that the "extracted" property is most likely a different subject than + /// the original one. /// /// It is recommended to give the extracted property a specific name by /// calling the `named` method. This helps with spotting the cause of a @@ -370,6 +370,9 @@ impl<'a, O, S> DerivedSpec<'a, O, S> { /// .extracting(|o| o.id) /// .is_equal_to("O261234"); /// ``` + /// + /// [`extracting_ref`]: Self::extracting_ref + /// [`mapping`]: Self::mapping #[must_use = "a derived spec does nothing unless an assertion method is called"] pub fn extracting(self, extract: F) -> DerivedSpec<'a, O, U> where diff --git a/src/lib.rs b/src/lib.rs index bbb79dd..dbad38f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -828,7 +828,7 @@ //! [`LengthProperty`]: properties::LengthProperty //! [`Spec`]: spec::Spec //! [`Spec::each_element()`]: spec::Spec::each_element -//! [`Spec::expecting()`]: spec::Spec::expecting +//! [`Spec::expecting()`]: spec::Expecting::expecting //! [`Spec::satisfies()`]: spec::Spec::satisfies //! [`SoftPanic::soft_panic()`]: spec::SoftPanic::soft_panic //! [`assert_that`]: spec::assert_that diff --git a/src/spec/mod.rs b/src/spec/mod.rs index 5bed173..5d6f466 100644 --- a/src/spec/mod.rs +++ b/src/spec/mod.rs @@ -865,11 +865,11 @@ impl<'a, S, R> Spec<'a, S, R> { /// multiple properties of the same subject, use the [`extracting_ref`] /// method instead. /// - /// This method is similar to the [`mapping()`](Spec::mapping) method. In - /// contrast to [`mapping()`](Spec::mapping), this method does not copy the - /// subject's name (or expression) but resets it to the default "subject". - /// The idea is that the "extracted" property is most likely a different - /// subject than the original one. + /// This method is similar to the [`mapping`] method. In contrast to + /// [`mapping`], this method does not copy the subject's name + /// (or expression) but resets it to the default "subject". The idea is + /// that the "extracted" property is most likely a different subject than + /// the original one. /// /// It is recommended to give the extracted property a specific name by /// calling the `named` method. This helps with spotting the cause of a @@ -896,6 +896,9 @@ impl<'a, S, R> Spec<'a, S, R> { /// .is_equal_to("imperdiet aliqua zzril eiusmod"); /// /// ``` + /// + /// [`extracting_ref`]: Self::extracting_ref + /// [`mapping`]: Self::mapping #[must_use = "a spec does nothing unless an assertion method is called"] pub fn extracting(self, extract: F) -> Spec<'a, U, R> where From 9deb9d9cb383b11f6af9a8cef7cab32fa5033158 Mon Sep 17 00:00:00 2001 From: haraldmaida Date: Sat, 18 Apr 2026 17:36:29 +0200 Subject: [PATCH 16/24] test: rename test module `iterator_extracted_elements_ref` to `extracted_elements_ref` --- src/iterator/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/iterator/tests.rs b/src/iterator/tests.rs index fdb6457..a6b6cfa 100644 --- a/src/iterator/tests.rs +++ b/src/iterator/tests.rs @@ -666,7 +666,7 @@ mod extracted_elements { } } -mod iterator_extracted_elements_ref { +mod extracted_elements_ref { use super::*; #[test] From 43b406fc23ff37ab6a645c07819429b3dc917965 Mon Sep 17 00:00:00 2001 From: haraldmaida Date: Sat, 18 Apr 2026 19:27:38 +0200 Subject: [PATCH 17/24] feat: implement `AssertOrderedElementsRef` for `DerivedSpec` --- src/derived_spec/mod.rs | 111 +++++++++++- src/derived_spec/tests.rs | 347 ++++++++++++++++++++++++++++++++++++++ src/iterator/tests.rs | 6 +- 3 files changed, 459 insertions(+), 5 deletions(-) diff --git a/src/derived_spec/mod.rs b/src/derived_spec/mod.rs index f0005de..9853d5b 100644 --- a/src/derived_spec/mod.rs +++ b/src/derived_spec/mod.rs @@ -8,8 +8,8 @@ use crate::assertions::{ AssertHasLength, AssertHasValue, AssertInRange, AssertInfinity, AssertIteratorContains, AssertIteratorContainsInAnyOrder, AssertIteratorContainsInOrder, AssertMapContainsKey, AssertMapContainsValue, AssertNotANumber, AssertNumericIdentity, AssertOption, - AssertOptionValue, AssertOrder, AssertOrderedElements, AssertResult, AssertResultValue, - AssertSameAs, AssertSignum, AssertStringContainsAnyOf, AssertStringPattern, + AssertOptionValue, AssertOrder, AssertOrderedElements, AssertOrderedElementsRef, AssertResult, + AssertResultValue, AssertSameAs, AssertSignum, AssertStringContainsAnyOf, AssertStringPattern, }; use crate::expectations::{ error_has_source, error_has_source_message, has_at_least_char_count, has_at_least_length, @@ -43,6 +43,7 @@ use crate::std::error::Error; use crate::std::fmt::{Debug, Display}; use crate::std::format; use crate::std::ops::RangeBounds; +use crate::std::slice; use crate::std::string::{String, ToString}; use crate::std::vec::Vec; use hashbrown::HashSet; @@ -455,6 +456,26 @@ impl<'a, O, S> DerivedSpec<'a, O, S> { } } +impl<'a, O, I> DerivedSpec<'a, O, I> +where + I: IntoIterator, +{ + pub(crate) fn extracting_ref_iter( + self, + property_name: impl Into>, + extract: F, + ) -> DerivedSpec<'a, DerivedSpec<'a, O, Vec<::Item>>, Vec> + where + for<'b> F: Fn(slice::Iter<'b, ::Item>) -> Vec, + { + let property_name = Expression(property_name.into()); + let diff_format = self.diff_format.clone(); + let orig_spec = self.mapping(Vec::from_iter); + let new_subject = extract(orig_spec.subject.iter()); + DerivedSpec::new(orig_spec, new_subject, property_name, diff_format) + } +} + impl Expecting for DerivedSpec<'_, O, S> where O: DoFail, @@ -1555,5 +1576,91 @@ where } } +impl<'a, O, S, T, U> AssertOrderedElementsRef for DerivedSpec<'a, O, S> +where + S: IntoIterator, + ::IntoIter: DefinedOrderProperty, + T: ToOwned + Debug, + O: DoFail + GetFailures, +{ + type SingleElement = DerivedSpec<'a, DerivedSpec<'a, O, Vec>, U>; + type MultipleElements = DerivedSpec<'a, DerivedSpec<'a, O, Vec>, Vec>; + + fn first_element_ref(self) -> Self::SingleElement { + let original_spec = self + .mapping(Vec::from_iter) + .expecting(has_at_least_number_of_elements(1)); + if original_spec.has_failures() { + PanicOnFail.do_fail_with(&original_spec.failures()); + unreachable!("Assertion failed and should have panicked! Please report a bug.") + } + let orig_subject_name = original_spec.expression(); + let new_subject_name = format!("the first element of {orig_subject_name}"); + original_spec.extracting_ref(new_subject_name, |collection| + collection.first() + .unwrap_or_else(|| + unreachable!("We should have asserted before, that there is at least one element in the collection/iterator. Please file a bug.") + ) + ) + } + + fn last_element_ref(self) -> Self::SingleElement { + let original_spec = self + .mapping(Vec::from_iter) + .expecting(has_at_least_number_of_elements(1)); + if original_spec.has_failures() { + PanicOnFail.do_fail_with(&original_spec.failures()); + unreachable!("Assertion failed and should have panicked! Please report a bug.") + } + let orig_subject_name = original_spec.expression(); + let new_subject_name = format!("the last element of {orig_subject_name}"); + original_spec.extracting_ref(new_subject_name, |collection| + collection.last() + .unwrap_or_else(|| + unreachable!("We should have asserted before, that there is at least one element in the collection/iterator. Please file a bug.") + ) + ) + } + + fn nth_element_ref(self, n: usize) -> Self::SingleElement { + let min_len = n + 1; + let original_spec = self + .mapping(Vec::from_iter) + .expecting(has_at_least_number_of_elements(min_len)); + if original_spec.has_failures() { + PanicOnFail.do_fail_with(&original_spec.failures()); + unreachable!("Assertion failed and should have panicked! Please report a bug.") + } + let orig_subject_name = original_spec.expression(); + let new_subject_name = format!("{orig_subject_name}[{n}]"); + original_spec.extracting_ref(new_subject_name, |collection| + collection.get(n) + .unwrap_or_else(|| + unreachable!("We should have asserted before, that there is at least one element in the collection/iterator. Please file a bug.") + ) + ) + } + + fn elements_ref_at(self, indices: impl IntoIterator) -> Self::MultipleElements { + let indices = Vec::from_iter(indices); + let orig_subject_name = self.expression(); + let new_subject_name = format!("{orig_subject_name} at positions {indices:?}"); + let indices = HashSet::<_>::from_iter(indices); + let original_spec = self.mapping(Vec::from_iter); + original_spec.extracting_ref_iter(new_subject_name, |collection| { + collection + .enumerate() + .filter_map(|(i, e)| { + if indices.contains(&i) { + Some(e.to_owned()) + } else { + None + } + }) + .collect() + }) + } +} + #[cfg(test)] mod tests; diff --git a/src/derived_spec/tests.rs b/src/derived_spec/tests.rs index cea53b8..6fba36a 100644 --- a/src/derived_spec/tests.rs +++ b/src/derived_spec/tests.rs @@ -913,3 +913,350 @@ fn extracting_ref_vec_contains_all_in_order() { .extracting_ref("names.0", |n| &n.0) .contains_all_in_order(["Silvia", "Robert"]); } + +mod iterator_extracted_elements_ref { + use super::*; + + #[allow(dead_code)] + struct Order { + id: u64, + items: Vec<&'static str>, + } + + #[test] + fn first_element_of_iterator_with_one_element() { + let order = Order { + id: 55, + items: vec!["Apple"], + }; + + assert_that(order) + .extracting_ref("order.items", |o| &o.items) + .first_element_ref() + .is_equal_to("Apple") + .has_length(5) + .starts_with("App") + .and() + .last_element_ref() + .is_equal_to("Apple"); + } + + #[test] + fn first_element_of_iterator_with_several_elements() { + let order = Order { + id: 55, + items: vec!["Apple", "Banana", "Cherry", "Grapes", "Orange"], + }; + + assert_that(order) + .extracting_ref("order.items", |o| &o.items) + .first_element_ref() + .is_equal_to("Apple") + .has_length(5) + .starts_with('A') + .and() + .last_element_ref() + .is_equal_to("Orange"); + } + + #[cfg(feature = "panic")] + #[test] + fn first_element_of_iterator_with_no_elements_fails() { + let order = Order { + id: 55, + items: vec![], + }; + + assert_that_code(|| { + assert_that(order) + .named("order") + .with_diff_format(DIFF_FORMAT_NO_HIGHLIGHT) + .extracting_ref("order.items", |o| &o.items) + .first_element_ref() + .is_equal_to("Apple"); + }) + .panics_with_message( + r"expected order.items to have at least one element, but has no elements + actual: [] +", + ); + } + + #[test] + fn verify_first_element_of_iterator_assertion_fails() { + let order = Order { + id: 55, + items: vec!["Melon", "Banana", "Cherry", "Grapes", "Orange"], + }; + + let failures = verify_that(order) + .named("order") + .extracting_ref("order.items", |o| &o.items) + .first_element_ref() + .is_equal_to("Apple") + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected the first element of order.items to be equal to "Apple" + but was: "Melon" + expected: "Apple" +"# + ] + ); + } + + #[test] + fn last_element_of_iterator_with_one_element() { + let order = Order { + id: 55, + items: vec!["Apple"], + }; + + assert_that(order) + .extracting_ref("order.items", |o| &o.items) + .last_element_ref() + .is_equal_to("Apple") + .has_length(5) + .starts_with("Ap") + .and() + .first_element_ref() + .is_equal_to("Apple"); + } + + #[test] + fn last_element_of_iterator_with_several_elements() { + let order = Order { + id: 55, + items: vec!["Apple", "Banana", "Cherry", "Grapes", "Orange"], + }; + + assert_that(order) + .extracting_ref("order.items", |o| &o.items) + .last_element_ref() + .is_equal_to("Orange") + .has_length(6) + .starts_with("Oran") + .and() + .first_element_ref() + .is_equal_to("Apple"); + } + + #[cfg(feature = "panic")] + #[test] + fn last_element_of_iterator_with_no_elements_fails() { + let order = Order { + id: 55, + items: vec![], + }; + + assert_that_code(|| { + assert_that(order) + .named("order") + .with_diff_format(DIFF_FORMAT_NO_HIGHLIGHT) + .extracting_ref("order.items", |o| &o.items) + .last_element_ref() + .is_equal_to("Grapes"); + }) + .panics_with_message( + r"expected order.items to have at least one element, but has no elements + actual: [] +", + ); + } + + #[test] + fn verify_last_element_of_iterator_assertion_fails() { + let order = Order { + id: 55, + items: vec!["Apple", "Banana", "Melon"], + }; + + let failures = verify_that(order) + .named("order") + .extracting_ref("order.items", |o| &o.items) + .last_element_ref() + .is_equal_to("Cherry") + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected the last element of order.items to be equal to "Cherry" + but was: "Melon" + expected: "Cherry" +"# + ] + ); + } + + #[test] + fn nth_element_of_iterator_with_one_element() { + let order = Order { + id: 55, + items: vec!["Apple"], + }; + + assert_that(order) + .extracting_ref("order.items", |o| &o.items) + .nth_element_ref(0) + .is_equal_to("Apple") + .has_length(5) + .starts_with("App") + .and() + .first_element_ref() + .is_equal_to("Apple"); + } + + #[test] + fn nth_element_of_iterator_with_several_elements_second_element() { + let order = Order { + id: 55, + items: vec!["Apple", "Banana", "Cherry", "Grapes", "Orange"], + }; + + assert_that(order) + .extracting_ref("order.items", |o| &o.items) + .nth_element_ref(1) + .is_equal_to("Banana") + .has_length(6) + .starts_with("Ban") + .and() + .nth_element_ref(3) + .is_equal_to("Grapes"); + } + + #[test] + fn nth_element_of_iterator_with_several_elements_fifth_element() { + let order = Order { + id: 55, + items: vec!["Apple", "Banana", "Cherry", "Grapes", "Orange"], + }; + + assert_that(order) + .extracting_ref("order.items", |o| &o.items) + .nth_element_ref(4) + .is_equal_to("Orange") + .has_length(6) + .starts_with("Or") + .and() + .nth_element_ref(0) + .is_equal_to("Apple"); + } + + #[cfg(feature = "panic")] + #[test] + fn nth_element_of_iterator_with_five_elements_6th_element_fails() { + let order = Order { + id: 55, + items: vec!["Apple", "Banana", "Cherry", "Grapes", "Orange"], + }; + + assert_that_code(|| { + assert_that(order) + .named("my_custom_collection") + .with_diff_format(DIFF_FORMAT_NO_HIGHLIGHT) + .extracting_ref("order.items", |o| &o.items) + .nth_element_ref(5) + .is_equal_to("Melon"); + }) + .panics_with_message( + r#"expected order.items to have at least 6 elements, but has 5 elements + actual: ["Apple", "Banana", "Cherry", "Grapes", "Orange"] +"#, + ); + } + + #[test] + fn verify_nth_element_of_iterator_assertion_fails() { + let order = Order { + id: 55, + items: vec!["Apple", "Melon", "Cherry", "Grapes", "Orange"], + }; + + let failures = verify_that(order) + .named("order") + .extracting_ref("order.items", |o| &o.items) + .nth_element_ref(1) + .is_equal_to("Banana") + .display_failures(); + + assert_eq!( + failures, + &[r#"expected order.items[1] to be equal to "Banana" + but was: "Melon" + expected: "Banana" +"#] + ); + } + + #[test] + fn elements_at_positions_of_iterator() { + let order = Order { + id: 55, + items: vec!["Apple", "Banana", "Cherry", "Grapes", "Orange"], + }; + + assert_that(order) + .extracting_ref("order.items", |o| &o.items) + .elements_ref_at([0, 2, 4]) + .contains_exactly(["Apple", "Cherry", "Orange"]); + } + + #[test] + fn verify_elements_at_positions_of_empty_iterator_fails() { + let order = Order { + id: 55, + items: vec![], + }; + + let failures = verify_that(order) + .named("order") + .extracting_ref("order.items", |o| &o.items) + .elements_ref_at([0, 1]) + .contains_exactly(["Apple", "Banana"]) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected order.items at positions [0, 1] to contain exactly in order ["Apple", "Banana"] + but was: [] + expected: ["Apple", "Banana"] + missing: ["Apple", "Banana"] + extra: [] + out-of-order: [] +"# + ] + ); + } + + #[test] + fn verify_elements_at_out_of_bounds_position_fails() { + let order = Order { + id: 55, + items: vec!["Apple", "Banana", "Cherry"], + }; + + let failures = verify_that(order) + .named("order") + .extracting_ref("order.items", |o| &o.items) + .elements_ref_at([0, 3]) + .contains_exactly(["Apple", "Grapes"]) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected order.items at positions [0, 3] to contain exactly in order ["Apple", "Grapes"] + but was: ["Apple"] + expected: ["Apple", "Grapes"] + missing: ["Grapes"] + extra: [] + out-of-order: [] +"# + ] + ); + } +} diff --git a/src/iterator/tests.rs b/src/iterator/tests.rs index a6b6cfa..b01ce49 100644 --- a/src/iterator/tests.rs +++ b/src/iterator/tests.rs @@ -711,7 +711,7 @@ mod extracted_elements_ref { } #[test] - fn verfiy_first_element_of_iterator_assertion_fails() { + fn verify_first_element_of_iterator_assertion_fails() { let subject = vec!["four", "two", "three"]; let failures = verify_that(subject) @@ -773,7 +773,7 @@ mod extracted_elements_ref { } #[test] - fn verfiy_last_element_of_iterator_assertion_fails() { + fn verify_last_element_of_iterator_assertion_fails() { let subject = vec!["one", "two", "four"]; let failures = verify_that(subject) @@ -846,7 +846,7 @@ mod extracted_elements_ref { } #[test] - fn verfiy_nth_element_of_iterator_assertion_fails() { + fn verify_nth_element_of_iterator_assertion_fails() { let subject = vec!["one", "four", "three"]; let failures = verify_that(subject) From 8936fc4ae478b8dca64900f030a632cd8177281c Mon Sep 17 00:00:00 2001 From: haraldmaida Date: Sat, 18 Apr 2026 19:28:10 +0200 Subject: [PATCH 18/24] doc: fix typo in "verfiy" --- README.md | 6 +++--- src/lib.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9d311aa..1f9f57a 100644 --- a/README.md +++ b/README.md @@ -457,9 +457,9 @@ for iterators that yield items in a well-defined order. | contains_all_in_order | verify that an iterator/collection contains all the given values and in the given order, possibly with other values between them | | starts_with | verify that an iterator/collection contains the given values as the first elements in order | | ends_with | verify that an iterator/collection contains the given values as the last elements in order | -| first_element | verfiy that an iterator/collection contains at least one element and return a `Spec` containing the first element | -| last_element | verfiy that an iterator/collection contains at least one element and return a `Spec` containing the last element | -| nth_element | verfiy that an iterator/collection contains at least one element and return a `Spec` containing the nth element | +| first_element | verify that an iterator/collection contains at least one element and return a `Spec` containing the first element | +| last_element | verify that an iterator/collection contains at least one element and return a `Spec` containing the last element | +| nth_element | verify that an iterator/collection contains at least one element and return a `Spec` containing the nth element | | elements_at | pick the elements of an iterator/collection at the given positions and return a `Spec` containing the selected elements | ### Maps diff --git a/src/lib.rs b/src/lib.rs index dbad38f..1e55222 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -464,7 +464,7 @@ //! ).is_equal_to(42); //! ``` //! -//! When using the `verfiy_*` variants of the macros or functions for each +//! When using the `verify_*` variants of the macros or functions for each //! failing assertion, a failure of type [`AssertFailure`] is added to the //! [`Spec`]. We can read the failures collected by calling the [`failures()`] //! method, like so: From e1921da8375118effaf9423c1a9def9bc4f03152 Mon Sep 17 00:00:00 2001 From: haraldmaida Date: Sun, 19 Apr 2026 20:04:26 +0200 Subject: [PATCH 19/24] refactor!: extract methods `satisfies` and `satisfies_with_message` from Spec into trait `Satisfies` and implement it for `Spec` and `DerivedSpec` --- src/derived_spec/mod.rs | 23 ++++- src/derived_spec/tests.rs | 44 +++++++++ src/lib.rs | 2 +- src/prelude.rs | 4 +- src/spec/mod.rs | 193 +++++++++++++++++++++----------------- 5 files changed, 174 insertions(+), 92 deletions(-) diff --git a/src/derived_spec/mod.rs b/src/derived_spec/mod.rs index 9853d5b..a01ac3d 100644 --- a/src/derived_spec/mod.rs +++ b/src/derived_spec/mod.rs @@ -26,7 +26,7 @@ use crate::expectations::{ iterator_contains_exactly_in_any_order, iterator_contains_only, iterator_contains_only_once, iterator_contains_sequence, iterator_ends_with, iterator_starts_with, map_contains_exactly_keys, map_contains_key, map_contains_keys, map_contains_value, - map_contains_values, map_does_not_contain_keys, map_does_not_contain_values, not, + map_contains_values, map_does_not_contain_keys, map_does_not_contain_values, not, satisfies, string_contains, string_contains_any_of, string_ends_with, string_starts_with, }; use crate::properties::{ @@ -36,7 +36,7 @@ use crate::properties::{ }; use crate::spec::{ And, AssertFailure, DiffFormat, DoFail, Expectation, Expecting, Expression, FailingStrategy, - GetFailures, PanicOnFail, SoftPanic, + GetFailures, PanicOnFail, Satisfies, SoftPanic, }; use crate::std::borrow::{Cow, ToOwned}; use crate::std::error::Error; @@ -476,6 +476,25 @@ where } } +impl Satisfies for DerivedSpec<'_, O, S> +where + O: DoFail, +{ + fn satisfies

(self, predicate: P) -> Self + where + P: Fn(&S) -> bool, + { + self.expecting(satisfies(predicate)) + } + + fn satisfies_with_message

(self, message: impl Into, predicate: P) -> Self + where + P: Fn(&S) -> bool, + { + self.expecting(satisfies(predicate).with_message(message)) + } +} + impl Expecting for DerivedSpec<'_, O, S> where O: DoFail, diff --git a/src/derived_spec/tests.rs b/src/derived_spec/tests.rs index 6fba36a..acc8c53 100644 --- a/src/derived_spec/tests.rs +++ b/src/derived_spec/tests.rs @@ -45,6 +45,8 @@ impl Person { } } +struct Answer(i32); + #[test] fn mapping_person_name_starts_with_alex() { let person = Person { @@ -198,6 +200,48 @@ fn extracting_ref_to_assert_all_order_item_fields() { .is_close_to(0.15); } +#[test] +fn assert_that_extracted_ref_satisfies_predicate() { + let answer = Answer(42); + + assert_that(answer) + .named("answer") + .extracting_ref("answer.val", |answer| &answer.0) + .satisfies(|actual| *actual == 42) + .is_at_least(42); +} + +#[test] +fn verify_that_subject_satisfies_predicate_fails() { + let subject = Answer(51); + + let failures = verify_that(subject) + .named("answer") + .extracting_ref("answer.val", |answer| &answer.0) + .satisfies(|actual| *actual == 42) + .display_failures(); + + assert_eq!( + failures, + &["expected answer.val to satisfy the given predicate, but returned false\n"] + ); +} + +#[test] +fn verify_that_subject_satisfies_predicate_fails_with_custom_message() { + let subject = Answer(51); + + let failures = verify_that(subject) + .named("answer") + .extracting_ref("answer.val", |answer| &answer.0) + .satisfies_with_message("the answer to all important questions is 42", |actual| { + *actual == 42 + }) + .display_failures(); + + assert_eq!(failures, &["the answer to all important questions is 42\n"]); +} + #[test] fn extracting_ref_string_is_equal_to() { struct Name(String); diff --git a/src/lib.rs b/src/lib.rs index 1e55222..287af20 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -829,7 +829,7 @@ //! [`Spec`]: spec::Spec //! [`Spec::each_element()`]: spec::Spec::each_element //! [`Spec::expecting()`]: spec::Expecting::expecting -//! [`Spec::satisfies()`]: spec::Spec::satisfies +//! [`Spec::satisfies()`]: spec::Satisfies::satisfies //! [`SoftPanic::soft_panic()`]: spec::SoftPanic::soft_panic //! [`assert_that`]: spec::assert_that //! [`assert_that_code`]: spec::assert_that_code diff --git a/src/prelude.rs b/src/prelude.rs index cb5f8b7..de8d260 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -20,8 +20,8 @@ pub use super::{ colored::{DEFAULT_DIFF_FORMAT, DIFF_FORMAT_NO_HIGHLIGHT}, properties::*, spec::{ - And, CollectFailures, DoFail, Expecting, GetFailures, Location, PanicOnFail, SoftPanic, - assert_that, verify_that, + And, CollectFailures, DoFail, Expecting, GetFailures, Location, PanicOnFail, Satisfies, + SoftPanic, assert_that, verify_that, }, verify_that, }; diff --git a/src/spec/mod.rs b/src/spec/mod.rs index 5d6f466..2e35f9a 100644 --- a/src/spec/mod.rs +++ b/src/spec/mod.rs @@ -967,93 +967,6 @@ impl<'a, S, R> Spec<'a, S, R> { } } -impl Spec<'_, S, R> -where - R: FailingStrategy, -{ - /// Asserts whether the given predicate is meet. - /// - /// This method takes a predicate function and calls it as an expectation. - /// In case the predicate function returns false, it does fail with a - /// generic failure message and according to the current failing strategy of - /// this `Spec`. - /// - /// This method can be used to do simple custom assertions without - /// implementing an [`Expectation`] and an assertion trait. - /// - /// # Examples - /// - /// ``` - /// use asserting::prelude::*; - /// - /// fn is_odd(value: &i32) -> bool { - /// value & 1 == 1 - /// } - /// - /// assert_that!(37).satisfies(is_odd); - /// - /// let failures = verify_that!(22).satisfies(is_odd).display_failures(); - /// - /// assert_that!(failures).contains_exactly([ - /// "expected 22 to satisfy the given predicate, but returned false\n" - /// ]); - /// ``` - /// - /// To assert a predicate with a custom failure message instead of the - /// generic one, use the method - /// [`satisfies_with_message`](Spec::satisfies_with_message). - #[allow(clippy::return_self_not_must_use)] - #[track_caller] - pub fn satisfies

(self, predicate: P) -> Self - where - P: Fn(&S) -> bool, - { - self.expecting(satisfies(predicate)) - } - - /// Asserts whether the given predicate is meet. - /// - /// This method takes a predicate function and calls it as an expectation. - /// In case the predicate function returns false, it does fail with the - /// provided failure message and according to the current failing strategy - /// of this `Spec`. - /// - /// This method can be used to do simple custom assertions without - /// implementing an [`Expectation`] and an assertion trait. - /// - /// # Examples - /// - /// ``` - /// use asserting::prelude::*; - /// - /// fn is_odd(value: &i32) -> bool { - /// value & 1 == 1 - /// } - /// - /// assert_that!(37).satisfies_with_message("expected my number to be odd", is_odd); - /// - /// let failures = verify_that!(22) - /// .satisfies_with_message("expected my number to be odd", is_odd) - /// .display_failures(); - /// - /// assert_that!(failures).contains_exactly([ - /// "expected my number to be odd\n" - /// ]); - /// ``` - /// - /// To assert a predicate with a generic failure message instead of - /// providing one use the method - /// [`satisfies`](Spec::satisfies). - #[allow(clippy::return_self_not_must_use)] - #[track_caller] - pub fn satisfies_with_message

(self, message: impl Into, predicate: P) -> Self - where - P: Fn(&S) -> bool, - { - self.expecting(satisfies(predicate).with_message(message)) - } -} - impl<'a, I, R> Spec<'a, I, R> where I: IntoIterator, @@ -1446,6 +1359,112 @@ impl And for Spec<'_, S, R> { } } +/// Verify whether a subject satisfies a given predicate. +/// +/// A predicate is a function that takes the subject and returns true if the +/// subject meets certain criteria. +pub trait Satisfies { + /// Asserts whether the given predicate is meet. + /// + /// A predicate is a function that takes the subject and returns true if the + /// subject meets certain criteria. + /// + /// This method takes a predicate function and calls it as an expectation. + /// In case the predicate function returns false, it does fail with a + /// generic failure message and according to the current failing strategy of + /// this `Spec`. + /// + /// This method can be used to do simple custom assertions without + /// implementing an [`Expectation`] and an assertion trait. + /// + /// # Examples + /// + /// ``` + /// use asserting::prelude::*; + /// + /// fn is_odd(value: &i32) -> bool { + /// value & 1 == 1 + /// } + /// + /// assert_that!(37).satisfies(is_odd); + /// + /// let failures = verify_that!(22).satisfies(is_odd).display_failures(); + /// + /// assert_that!(failures).contains_exactly([ + /// "expected 22 to satisfy the given predicate, but returned false\n" + /// ]); + /// ``` + /// + /// To assert a predicate with a custom failure message instead of the + /// generic one, use the method + /// [`satisfies_with_message`](Spec::satisfies_with_message). + #[allow(clippy::return_self_not_must_use)] + #[track_caller] + fn satisfies

(self, predicate: P) -> Self + where + P: Fn(&S) -> bool; + + /// Asserts whether the given predicate is meet. + /// + /// A predicate is a function that takes the subject and returns true if the + /// subject meets certain criteria. + /// + /// This method takes a predicate function and calls it as an expectation. + /// In case the predicate function returns false, it does fail with the + /// provided failure message and according to the current failing strategy + /// of this `Spec`. + /// + /// This method can be used to do simple custom assertions without + /// implementing an [`Expectation`] and an assertion trait. + /// + /// # Examples + /// + /// ``` + /// use asserting::prelude::*; + /// + /// fn is_odd(value: &i32) -> bool { + /// value & 1 == 1 + /// } + /// + /// assert_that!(37).satisfies_with_message("expected my number to be odd", is_odd); + /// + /// let failures = verify_that!(22) + /// .satisfies_with_message("expected my number to be odd", is_odd) + /// .display_failures(); + /// + /// assert_that!(failures).contains_exactly([ + /// "expected my number to be odd\n" + /// ]); + /// ``` + /// + /// To assert a predicate with a generic failure message instead of + /// providing one, use the method [`satisfies`](Satisfies::satisfies). + #[allow(clippy::return_self_not_must_use)] + #[track_caller] + fn satisfies_with_message

(self, message: impl Into, predicate: P) -> Self + where + P: Fn(&S) -> bool; +} + +impl Satisfies for Spec<'_, S, R> +where + R: FailingStrategy, +{ + fn satisfies

(self, predicate: P) -> Self + where + P: Fn(&S) -> bool, + { + self.expecting(satisfies(predicate)) + } + + fn satisfies_with_message

(self, message: impl Into, predicate: P) -> Self + where + P: Fn(&S) -> bool, + { + self.expecting(satisfies(predicate).with_message(message)) + } +} + /// Verify whether a subject meets the given expectation (impl of /// [`Expectation`]) and record a failure if it is not met. pub trait Expecting { From 345dfc92f4bb4ea0dab9a57ea089aa768e8017a0 Mon Sep 17 00:00:00 2001 From: haraldmaida Date: Sun, 19 Apr 2026 20:22:38 +0200 Subject: [PATCH 20/24] chore: fix run configuration 'Doc' for Jetbrains IDEs --- .ide-settings/jetbrains/runConfigurations/Doc.run.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.ide-settings/jetbrains/runConfigurations/Doc.run.xml b/.ide-settings/jetbrains/runConfigurations/Doc.run.xml index 6a9cbbf..62fb27d 100644 --- a/.ide-settings/jetbrains/runConfigurations/Doc.run.xml +++ b/.ide-settings/jetbrains/runConfigurations/Doc.run.xml @@ -1,10 +1,10 @@ -