From 5abc125305d0cfb71a076fda0f2f8a9d11e89b62 Mon Sep 17 00:00:00 2001 From: haraldmaida Date: Fri, 26 Dec 2025 22:25:54 +0100 Subject: [PATCH 1/5] feat: implement filter assertions `single_element`, `first_element`, `last_element` and `nth_element` for collections and iterators --- src/assertions.rs | 92 ++++++++++++++++ src/expectations.rs | 20 ++++ src/iterator/mod.rs | 157 ++++++++++++++++++++++++++-- src/iterator/tests.rs | 237 ++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 19 +++- src/spec/mod.rs | 5 + 6 files changed, 519 insertions(+), 11 deletions(-) diff --git a/src/assertions.rs b/src/assertions.rs index ff61235..59efb41 100644 --- a/src/assertions.rs +++ b/src/assertions.rs @@ -3651,3 +3651,95 @@ pub trait AssertMapContainsValue { #[track_caller] fn does_not_contain_values(self, expected_values: impl IntoIterator) -> Self; } + +/// Filter assertions for elements of a collection or an iterator. +/// +/// Filtering is used to target the assertions on specific elements of a +/// collection or an iterator, such as a single element. +/// +/// # Examples +/// +/// ``` +/// use asserting::prelude::*; +/// +/// let subject = ["single"]; +/// assert_that!(subject).single_element().is_equal_to("single"); +/// ``` +pub trait AssertElements<'a, T, R> { + /// Verify that the iterator contains exactly one element and return a + /// [`Spec`] for that single element. + /// + /// # Examples + /// + /// ``` + /// use asserting::prelude::*; + /// + /// let subject = ["single"]; + /// assert_that!(subject).single_element().is_equal_to("single"); + /// ``` + #[track_caller] + fn single_element(self) -> Spec<'a, T, R>; +} + +/// Filter assertions for elements of a collection or an iterator that yields +/// its elements in a defined order. +/// +/// Filtering is used to target the assertions on specific elements of a +/// collection or an iterator, such as the first or last element. +/// +/// # Examples +/// +/// ``` +/// use asserting::prelude::*; +/// +/// let subject = ["first", "second", "third"]; +/// assert_that!(subject).first_element().is_equal_to("first"); +/// assert_that!(subject).last_element().is_equal_to("third"); +/// ``` +pub trait AssertOrderedElements<'a, T, R> { + /// 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 = ["first", "second", "third"]; + /// assert_that!(subject).first_element().is_equal_to("first"); + /// ``` + #[track_caller] + fn first_element(self) -> Spec<'a, T, R>; + + /// 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 = ["first", "second", "third"]; + /// assert_that!(subject).last_element().is_equal_to("third"); + /// ``` + #[track_caller] + fn last_element(self) -> Spec<'a, T, R>; + + /// 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 = ["first", "second", "third"]; + /// 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"); + /// ``` + #[track_caller] + fn nth_element(self, n: usize) -> Spec<'a, T, R>; +} diff --git a/src/expectations.rs b/src/expectations.rs index 95b97a4..b067f75 100644 --- a/src/expectations.rs +++ b/src/expectations.rs @@ -1177,6 +1177,26 @@ impl IteratorEndsWith { } } +pub fn has_single_element() -> HasSingleElement { + HasSingleElement +} + +#[must_use] +pub struct HasSingleElement; + +pub fn has_at_least_number_of_elements( + expected_number_of_elements: usize, +) -> HasAtLeastNumberOfElements { + HasAtLeastNumberOfElements { + expected_number_of_elements, + } +} + +#[must_use] +pub struct HasAtLeastNumberOfElements { + pub expected_number_of_elements: usize, +} + /// Creates a [`MapContainsKey`] expectation. pub fn map_contains_key(expected_key: E) -> MapContainsKey { MapContainsKey { expected_key } diff --git a/src/iterator/mod.rs b/src/iterator/mod.rs index 94ab350..5f59f19 100644 --- a/src/iterator/mod.rs +++ b/src/iterator/mod.rs @@ -1,22 +1,28 @@ //! Implementations of assertions for `Iterator` values. use crate::assertions::{ - AssertIteratorContains, AssertIteratorContainsInAnyOrder, AssertIteratorContainsInOrder, + AssertElements, AssertIteratorContains, AssertIteratorContainsInAnyOrder, + AssertIteratorContainsInOrder, }; use crate::colored::{ - mark_all_items_in_collection, mark_missing, mark_selected_items_in_collection, mark_unexpected, + mark_all_items_in_collection, mark_missing, mark_missing_string, + mark_selected_items_in_collection, mark_unexpected, mark_unexpected_string, }; use crate::expectations::{ - 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, IteratorContains, IteratorContainsAllInOrder, - IteratorContainsAllOf, IteratorContainsAnyOf, IteratorContainsExactly, - IteratorContainsExactlyInAnyOrder, IteratorContainsOnly, IteratorContainsOnlyOnce, - IteratorContainsSequence, IteratorEndsWith, IteratorStartsWith, + has_at_least_number_of_elements, has_single_element, 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, HasAtLeastNumberOfElements, HasSingleElement, IteratorContains, + IteratorContainsAllInOrder, IteratorContainsAllOf, IteratorContainsAnyOf, + IteratorContainsExactly, IteratorContainsExactlyInAnyOrder, IteratorContainsOnly, + IteratorContainsOnlyOnce, IteratorContainsSequence, IteratorEndsWith, IteratorStartsWith, }; +use crate::prelude::AssertOrderedElements; use crate::properties::DefinedOrderProperty; -use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec}; +use crate::spec::{ + DiffFormat, Expectation, Expression, FailingStrategy, Invertible, PanicOnFail, Spec, +}; use crate::std::cmp::Ordering; use crate::std::fmt::Debug; use crate::std::mem; @@ -770,6 +776,137 @@ where } } +impl Expectation> for HasSingleElement +where + T: Debug, +{ + fn test(&mut self, subject: &Vec) -> bool { + subject.len() == 1 + } + + fn message( + &self, + expression: &Expression<'_>, + actual: &Vec, + _inverted: bool, + format: &DiffFormat, + ) -> String { + let actual_length = actual.len(); + let actual_elements = match actual_length { + 0 => mark_unexpected_string("no elements", format), + 1 => mark_unexpected_string("exactly one element", format), + _ => mark_unexpected_string(&format!("{actual_length} elements"), format), + }; + let expected_elements = mark_missing_string("exactly one element", format); + format!( + r"expected {expression} to have {expected_elements}, but has {actual_elements} + actual: {actual:?}" + ) + } +} + +impl<'a, S, T, R> AssertElements<'a, T, R> for Spec<'a, S, R> +where + S: IntoIterator, + T: Debug, + R: FailingStrategy, +{ + fn single_element(self) -> Spec<'a, T, R> { + let spec = self.mapping(Vec::from_iter).expecting(has_single_element()); + 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.") + }) + }) + } +} + +impl Expectation> for HasAtLeastNumberOfElements +where + T: Debug, +{ + fn test(&mut self, subject: &Vec) -> bool { + subject.len() >= self.expected_number_of_elements + } + + fn message( + &self, + expression: &Expression<'_>, + actual: &Vec, + _inverted: bool, + format: &DiffFormat, + ) -> String { + let actual_length = actual.len(); + let actual_elements = match actual_length { + 0 => mark_unexpected_string("no elements", format), + 1 => mark_unexpected_string("one element", format), + _ => mark_unexpected_string(&format!("{actual_length} elements"), format), + }; + let expected_elements = match self.expected_number_of_elements { + 0 => mark_missing_string("no elements", format), + 1 => mark_missing_string("at least one element", format), + _ => mark_missing_string( + &format!("at least {} elements", self.expected_number_of_elements), + format, + ), + }; + format!( + r"expected {expression} to have {expected_elements}, but has {actual_elements} + actual: {actual:?}" + ) + } +} + +impl<'a, S, T, R> AssertOrderedElements<'a, T, R> for Spec<'a, S, R> +where + S: IntoIterator, + ::IntoIter: DefinedOrderProperty, + T: Debug, + R: FailingStrategy, +{ + fn first_element(self) -> Spec<'a, T, R> { + 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) -> Spec<'a, T, R> { + 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) -> Spec<'a, T, R> { + 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)) + } +} + pub fn collect_selected_values<'a, T>(indices: &HashSet, collection: &'a [T]) -> Vec<&'a T> { collection .iter() diff --git a/src/iterator/tests.rs b/src/iterator/tests.rs index 3194bfb..a8a89d7 100644 --- a/src/iterator/tests.rs +++ b/src/iterator/tests.rs @@ -41,6 +41,36 @@ impl LengthProperty for CustomCollection { } } +#[derive(Debug)] +struct CustomOrderedCollection { + inner: Vec, +} + +struct CustomOrderedIter { + inner: vec::IntoIter, +} + +impl Iterator for CustomOrderedIter { + type Item = T; + + fn next(&mut self) -> Option { + self.inner.next() + } +} + +impl IntoIterator for CustomOrderedCollection { + type Item = T; + type IntoIter = CustomOrderedIter; + + fn into_iter(self) -> Self::IntoIter { + CustomOrderedIter { + inner: self.inner.into_iter(), + } + } +} + +impl DefinedOrderProperty for CustomOrderedIter {} + #[test] fn custom_collection_is_empty() { let subject: CustomCollection = CustomCollection { inner: vec![] }; @@ -200,3 +230,210 @@ fn verify_custom_iterator_does_not_contain_fails() { "] ); } + +mod element_filters { + use super::*; + + #[test] + fn single_element_of_iterator_with_one_element() { + let subject = CustomCollection { + inner: vec!["single"], + }; + + assert_that(subject) + .single_element() + .is_equal_to("single") + .has_length(6) + .starts_with('s'); + } + + #[cfg(feature = "panic")] + #[test] + fn single_element_of_empty_iterator_fails() { + let subject: CustomCollection = CustomCollection { inner: vec![] }; + + assert_that_code(|| { + assert_that(subject) + .named("my_custom_collection") + .with_diff_format(DIFF_FORMAT_NO_HIGHLIGHT) + .single_element() + .is_equal_to(42); + }) + .panics_with_message( + r"expected my_custom_collection to have exactly one element, but has no elements + actual: [] +", + ); + } + + #[cfg(feature = "panic")] + #[test] + fn single_element_of_iterator_with_2_elements_fails() { + let subject: CustomCollection = CustomCollection { + inner: vec![42, -1], + }; + + assert_that_code(|| { + assert_that(subject) + .named("my_custom_collection") + .with_diff_format(DIFF_FORMAT_NO_HIGHLIGHT) + .single_element() + .is_equal_to(42); + }) + .panics_with_message( + r"expected my_custom_collection to have exactly one element, but has 2 elements + actual: [42, -1] +", + ); + } + + #[test] + fn first_element_of_iterator_with_one_element() { + let subject = CustomOrderedCollection { + inner: vec!["single"], + }; + + assert_that(subject) + .first_element() + .is_equal_to("single") + .has_length(6) + .starts_with("si"); + } + + #[test] + fn first_element_of_iterator_with_several_elements() { + let subject = CustomOrderedCollection { + inner: vec!["one", "two", "three", "four", "five"], + }; + + assert_that(subject) + .first_element() + .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: CustomOrderedCollection = CustomOrderedCollection { inner: vec![] }; + + assert_that_code(|| { + assert_that(subject) + .named("my_custom_collection") + .with_diff_format(DIFF_FORMAT_NO_HIGHLIGHT) + .first_element() + .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 last_element_of_iterator_with_one_element() { + let subject = CustomOrderedCollection { + inner: vec!["single"], + }; + + assert_that(subject) + .last_element() + .is_equal_to("single") + .has_length(6) + .starts_with("si"); + } + + #[test] + fn last_element_of_iterator_with_several_elements() { + let subject = CustomOrderedCollection { + inner: vec!["one", "two", "three", "four", "five"], + }; + + assert_that(subject) + .last_element() + .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: CustomOrderedCollection = CustomOrderedCollection { inner: vec![] }; + + assert_that_code(|| { + assert_that(subject) + .named("my_custom_collection") + .with_diff_format(DIFF_FORMAT_NO_HIGHLIGHT) + .last_element() + .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 nth_element_of_iterator_with_one_element() { + let subject = CustomOrderedCollection { + inner: vec!["single"], + }; + + assert_that(subject) + .nth_element(0) + .is_equal_to("single") + .has_length(6) + .starts_with("si"); + } + + #[test] + fn nth_element_of_iterator_with_several_elements_second_element() { + let subject = CustomOrderedCollection { + inner: vec!["one", "two", "three", "four", "five"], + }; + + assert_that(subject) + .nth_element(1) + .is_equal_to("two") + .has_length(3) + .starts_with("tw"); + } + + #[test] + fn nth_element_of_iterator_with_several_elements_fifth_element() { + let subject = CustomOrderedCollection { + inner: vec!["one", "two", "three", "four", "five"], + }; + + assert_that(subject) + .nth_element(4) + .is_equal_to("five") + .has_length(4) + .starts_with("fi"); + } + + #[cfg(feature = "panic")] + #[test] + fn nth_element_of_iterator_with_five_elements_6th_element_fails() { + let subject = CustomOrderedCollection { + inner: 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(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"] +"#, + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index 957fe66..638de48 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -125,7 +125,7 @@ //! .contains_only([1, 3, 5, 7, 9, 11, 13, 17, 19, 23, 29, 31, 37, 43]); //! ``` //! -//! ## Asserting each item of a collection or iterator +//! ## Asserting each item of a collection or an iterator //! //! ``` //! use asserting::prelude::*; @@ -140,6 +140,23 @@ //! //! For more details see [`Spec::each_item()`]. //! +//! ## Asserting specific elements of a collection or an iterator +//! +//! Filter assertions are handy to assert a specific element of a collection or +//! an iterator. +//! +//! ``` +//! use asserting::prelude::*; +//! +//! let subject = ["single"]; +//! assert_that!(subject).single_element().is_equal_to("single"); +//! +//! let numbers = [1, 2, 3, 4, 5]; +//! assert_that!(numbers).first_element().is_equal_to(1); +//! assert_that!(numbers).last_element().is_equal_to(5); +//! assert_that!(numbers).nth_element(3).is_equal_to(4); +//! ``` +//! //! ## Soft assertions //! //! ```should_panic diff --git a/src/spec/mod.rs b/src/spec/mod.rs index 0f7f0a1..bd27d0d 100644 --- a/src/spec/mod.rs +++ b/src/spec/mod.rs @@ -669,6 +669,11 @@ impl Spec<'_, S, R> { &self.failing_strategy } + /// Returns whether there are assertion failures collected so far. + pub fn has_failures(&self) -> bool { + !self.failures.is_empty() + } + /// Returns the assertion failures that have been collected so far. pub fn failures(&self) -> Vec { self.failures.clone() From ad4b2dfc24c97ec295b5e70fc3f34c9e34fa1c0f Mon Sep 17 00:00:00 2001 From: haraldmaida Date: Fri, 26 Dec 2025 23:05:13 +0100 Subject: [PATCH 2/5] feat: implement filter assertion `filtered_on` for collections and iterators --- src/assertions.rs | 29 ++++++++++++++++++++++++++++- src/iterator/mod.rs | 7 +++++-- src/iterator/tests.rs | 11 +++++++++++ src/lib.rs | 13 ++++++++++++- 4 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/assertions.rs b/src/assertions.rs index 59efb41..ccf947f 100644 --- a/src/assertions.rs +++ b/src/assertions.rs @@ -14,6 +14,7 @@ use crate::spec::Spec; use crate::std::fmt::Debug; use crate::std::ops::RangeBounds; use crate::std::string::String; +use crate::std::vec::Vec; /// Assert whether two values are equal or not. /// @@ -3655,7 +3656,8 @@ pub trait AssertMapContainsValue { /// Filter assertions for elements of a collection or an iterator. /// /// Filtering is used to target the assertions on specific elements of a -/// collection or an iterator, such as a single element. +/// collection or an iterator, such as a single element or elements matching a +/// condition. /// /// # Examples /// @@ -3664,6 +3666,9 @@ pub trait AssertMapContainsValue { /// /// let subject = ["single"]; /// assert_that!(subject).single_element().is_equal_to("single"); +/// +/// let subject = [1, 2, 3, 4, 5]; +/// assert_that!(subject).filtered_on(|e| e & 1 == 0).contains_exactly_in_any_order([2, 4]); /// ``` pub trait AssertElements<'a, T, R> { /// Verify that the iterator contains exactly one element and return a @@ -3679,6 +3684,28 @@ pub trait AssertElements<'a, T, R> { /// ``` #[track_caller] fn single_element(self) -> Spec<'a, T, R>; + + /// Filter the elements of a collection or an iterator on a condition and + /// return a [`Spec`] only containing the elements that match the condition. + /// + /// # Examples + /// + /// ``` + /// use asserting::prelude::*; + /// + /// let subject = [1, 2, 3, 4, 5]; + /// assert_that!(subject) + /// .filtered_on(|e| e & 1 == 0) + /// .contains_exactly_in_any_order([2, 4]); + /// + /// let subject = ["one", "two", "three", "four"]; + /// assert_that!(subject) + /// .filtered_on(|e| e.len() == 5) + /// .single_element() + /// .is_equal_to("three"); + /// ``` + #[track_caller] + fn filtered_on(self, condition: impl Fn(&T) -> bool) -> Spec<'a, Vec, R>; } /// Filter assertions for elements of a collection or an iterator that yields diff --git a/src/iterator/mod.rs b/src/iterator/mod.rs index 5f59f19..eb396b9 100644 --- a/src/iterator/mod.rs +++ b/src/iterator/mod.rs @@ -2,7 +2,7 @@ use crate::assertions::{ AssertElements, AssertIteratorContains, AssertIteratorContainsInAnyOrder, - AssertIteratorContainsInOrder, + AssertIteratorContainsInOrder, AssertOrderedElements, }; use crate::colored::{ mark_all_items_in_collection, mark_missing, mark_missing_string, @@ -18,7 +18,6 @@ use crate::expectations::{ IteratorContainsExactly, IteratorContainsExactlyInAnyOrder, IteratorContainsOnly, IteratorContainsOnlyOnce, IteratorContainsSequence, IteratorEndsWith, IteratorStartsWith, }; -use crate::prelude::AssertOrderedElements; use crate::properties::DefinedOrderProperty; use crate::spec::{ DiffFormat, Expectation, Expression, FailingStrategy, Invertible, PanicOnFail, Spec, @@ -823,6 +822,10 @@ where }) }) } + + fn filtered_on(self, condition: impl Fn(&T) -> bool) -> Spec<'a, Vec, R> { + self.mapping(|subject| subject.into_iter().filter(condition).collect()) + } } impl Expectation> for HasAtLeastNumberOfElements diff --git a/src/iterator/tests.rs b/src/iterator/tests.rs index a8a89d7..870a378 100644 --- a/src/iterator/tests.rs +++ b/src/iterator/tests.rs @@ -436,4 +436,15 @@ mod element_filters { "#, ); } + + #[test] + fn filtered_on_elements_of_iterator_event_elements() { + let subject = CustomOrderedCollection { + inner: vec![1, 2, 3, 4, 5], + }; + + assert_that(subject) + .filtered_on(|e| e & 1 == 0) + .contains_exactly_in_any_order([2, 4]); + } } diff --git a/src/lib.rs b/src/lib.rs index 638de48..91bae53 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -142,7 +142,7 @@ //! //! ## Asserting specific elements of a collection or an iterator //! -//! Filter assertions are handy to assert a specific element of a collection or +//! Filter assertions are handy to assert specific elements of a collection or //! an iterator. //! //! ``` @@ -155,6 +155,17 @@ //! assert_that!(numbers).first_element().is_equal_to(1); //! assert_that!(numbers).last_element().is_equal_to(5); //! assert_that!(numbers).nth_element(3).is_equal_to(4); +//! +//! let subject = [1, 2, 3, 4, 5]; +//! assert_that!(subject) +//! .filtered_on(|e| e & 1 == 0) +//! .contains_exactly_in_any_order([2, 4]); +//! +//! let subject = ["one", "two", "three", "four"]; +//! assert_that!(subject) +//! .filtered_on(|e| e.len() == 5) +//! .single_element() +//! .is_equal_to("three"); //! ``` //! //! ## Soft assertions From 4092bb8b896978756f2319d9b0b829a25b54188d Mon Sep 17 00:00:00 2001 From: haraldmaida Date: Fri, 26 Dec 2025 23:35:38 +0100 Subject: [PATCH 3/5] feat: implement filter assertion `elements_at` for ordered collections and iterators --- src/assertions.rs | 33 +++++++++++++++++++++++++++++++-- src/iterator/mod.rs | 11 +++++++++++ src/iterator/tests.rs | 15 +++++++++++++-- src/lib.rs | 30 ++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 4 deletions(-) diff --git a/src/assertions.rs b/src/assertions.rs index ccf947f..25bf397 100644 --- a/src/assertions.rs +++ b/src/assertions.rs @@ -3680,6 +3680,7 @@ pub trait AssertElements<'a, T, R> { /// use asserting::prelude::*; /// /// let subject = ["single"]; + /// /// assert_that!(subject).single_element().is_equal_to("single"); /// ``` #[track_caller] @@ -3719,9 +3720,17 @@ pub trait AssertElements<'a, T, R> { /// ``` /// use asserting::prelude::*; /// -/// let subject = ["first", "second", "third"]; +/// let subject = ["first", "second", "third", "four", "five"]; +/// /// assert_that!(subject).first_element().is_equal_to("first"); -/// assert_that!(subject).last_element().is_equal_to("third"); +/// 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"]); /// ``` pub trait AssertOrderedElements<'a, T, R> { /// Verify that a collection or an iterator contains at least one element @@ -3733,6 +3742,7 @@ pub trait AssertOrderedElements<'a, T, R> { /// use asserting::prelude::*; /// /// let subject = ["first", "second", "third"]; + /// /// assert_that!(subject).first_element().is_equal_to("first"); /// ``` #[track_caller] @@ -3747,6 +3757,7 @@ pub trait AssertOrderedElements<'a, T, R> { /// use asserting::prelude::*; /// /// let subject = ["first", "second", "third"]; + /// /// assert_that!(subject).last_element().is_equal_to("third"); /// ``` #[track_caller] @@ -3763,10 +3774,28 @@ pub trait AssertOrderedElements<'a, T, R> { /// use asserting::prelude::*; /// /// let subject = ["first", "second", "third"]; + /// /// 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"); /// ``` #[track_caller] fn nth_element(self, n: usize) -> Spec<'a, T, R>; + + /// 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_at([0, 2, 4]) + /// .contains_exactly(["one", "three", "five"]); + /// ``` + #[track_caller] + fn elements_at(self, indices: impl IntoIterator) -> Spec<'a, Vec, R>; } diff --git a/src/iterator/mod.rs b/src/iterator/mod.rs index eb396b9..fece921 100644 --- a/src/iterator/mod.rs +++ b/src/iterator/mod.rs @@ -908,6 +908,17 @@ where } spec.extracting(|mut collection| collection.remove(n)) } + + fn elements_at(self, indices: impl IntoIterator) -> Spec<'a, Vec, R> { + 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() + }) + } } pub fn collect_selected_values<'a, T>(indices: &HashSet, collection: &'a [T]) -> Vec<&'a T> { diff --git a/src/iterator/tests.rs b/src/iterator/tests.rs index 870a378..c0d9794 100644 --- a/src/iterator/tests.rs +++ b/src/iterator/tests.rs @@ -438,8 +438,8 @@ mod element_filters { } #[test] - fn filtered_on_elements_of_iterator_event_elements() { - let subject = CustomOrderedCollection { + fn filtered_on_elements_of_iterator_even_elements() { + let subject = CustomCollection { inner: vec![1, 2, 3, 4, 5], }; @@ -447,4 +447,15 @@ mod element_filters { .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"]); + } } diff --git a/src/lib.rs b/src/lib.rs index 91bae53..31fdb9e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -145,29 +145,59 @@ //! Filter assertions are handy to assert specific elements of a collection or //! an iterator. //! +//! Assert the only element of a collection or an iterator: +//! //! ``` //! use asserting::prelude::*; //! //! let subject = ["single"]; +//! //! assert_that!(subject).single_element().is_equal_to("single"); +//! ``` +//! +//! Assert the first, the last, or the nth element of a collection or an iterator: +//! +//! ``` +//! use asserting::prelude::*; //! //! let numbers = [1, 2, 3, 4, 5]; +//! //! assert_that!(numbers).first_element().is_equal_to(1); //! assert_that!(numbers).last_element().is_equal_to(5); //! assert_that!(numbers).nth_element(3).is_equal_to(4); +//! ``` +//! +//! Filter the elements to be asserted on a condition: +//! +//! ``` +//! use asserting::prelude::*; //! //! let subject = [1, 2, 3, 4, 5]; +//! //! assert_that!(subject) //! .filtered_on(|e| e & 1 == 0) //! .contains_exactly_in_any_order([2, 4]); //! //! let subject = ["one", "two", "three", "four"]; +//! //! assert_that!(subject) //! .filtered_on(|e| e.len() == 5) //! .single_element() //! .is_equal_to("three"); //! ``` //! +//! Pick the elements of a collection or an iterator at given positions: +//! +//! ``` +//! use asserting::prelude::*; +//! +//! let subject = ["one", "two", "three", "four", "five"]; +//! +//! assert_that!(subject) +//! .elements_at([0, 2, 4]) +//! .contains_exactly(["one", "three", "five"]); +//! ``` +//! //! ## Soft assertions //! //! ```should_panic From 1bcef7115abe677a42f704c840e50f3383822a70 Mon Sep 17 00:00:00 2001 From: haraldmaida Date: Fri, 26 Dec 2025 23:54:54 +0100 Subject: [PATCH 4/5] doc: document filter assertions for iterators in README --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6ed8561..f69eb79 100644 --- a/README.md +++ b/README.md @@ -414,11 +414,11 @@ for all iterators. | contains_all_of | verify that an iterator/collection contains all the expected values in any order (and maybe more) | | contains_only | verify that an iterator/collection contains only the specified values and nothing else in any order and ignoring duplicates | | contains_only_once | verify that an iterator/collection contains only the specified values in any order and each of them only once | +| single_element | verify that an iterator/collection contains exaclty one element and return a `Spec` for that one element | +| filtered_on | filter the elements of an iterator/collection on a condition and return a `Spec` that contains the filtered elements | for iterators that yield items in a well-defined order. -All the above assertions provided for any kind of iterator plus the following: - | assertion | description | |-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------| | contains_exactly | verify that an iterator/collection contains exactly the expected values and nothing else in the given order | @@ -426,6 +426,10 @@ All the above assertions provided for any kind of iterator plus the following: | 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 | +| elements_at | pick the elements of an iterator/collection at the given positions and return a `Spec` containing the selected elements | ### Maps From 1f0a2b67c76e1227ac431b2d5963d741e1bddfbe Mon Sep 17 00:00:00 2001 From: haraldmaida Date: Fri, 26 Dec 2025 23:55:36 +0100 Subject: [PATCH 5/5] doc: mention inspiration by the AssertJ library in README --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f69eb79..3c2c03e 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,9 @@ Features of `asserting`: For an overview of the provided features and many examples on how to use `asserting` see the [crate-level documentation][docs-url]. +The assertion methods are mostly inspired by the [AssertJ] library for Java but adopted to Rust's +syntax and idioms. + To see what is changed with each release, see the [changelog](CHANGELOG.md). ### Convenient to write @@ -70,7 +73,7 @@ Easy-to-extend means that we can write assertions for custom types with minimal 3. write custom assertions by implementing two simple traits (see "[custom assertions]") The mentioned references link to a chapter in the crate's documentation that describes the -possibilities for custom assertions including examples. +possibilities for custom assertions, including examples. ## no-std support @@ -460,7 +463,7 @@ requires the crate feature `panic` which is enabled by default. | panics | verify that some code panics | | panics_with_message | verify that some code panics with the expected message | -To start assertions on code use the `assert_that_code!()` macro. +To start assertions on code, use the `assert_that_code!()` macro. @@ -501,3 +504,5 @@ To start assertions on code use the `assert_that_code!()` macro. [`MapProperties`]: https://docs.rs/asserting/latest/asserting/properties/trait.MapProperties.html [`NO_COLOR`]: https://no-color.org/ + +[AssertJ]: https://assertj.github.io/doc/