Skip to content

Commit e83dec5

Browse files
committed
feat!: add parameter property_name to Spec::extracting method
BREAKING-CHANGE: In previous versions the `Spec::extracting` method set the property_name (field `expression` in `Spec`) to the default value `"subject"` and the user had to call the `Spec::named` method explicitly each time after calling the extracting method to get a usefull subject-name in the error report. Users that picked up `asserting` recently might not know yet about the possiblity of the `named` method. For expierience users of `asserting` it is very inconvient to always remember to call the `named` method after calling `extracting`. Therefore the `property_name` is now a mandatory parameter. It provides an additional advantage, as it concatenates the provided `property_name` to the original subject to form a kind of property path, like `"original_subject.extracted_property"`.
1 parent b93b6fd commit e83dec5

9 files changed

Lines changed: 190 additions & 99 deletions

File tree

examples/assertion_function.rs

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -64,28 +64,21 @@ struct Snake {
6464
/// "snake.body" and "snake.head".
6565
#[track_caller]
6666
fn assert_snake_body(snake: &Snake, expected_body: &[Coord]) {
67-
let mut failures = verify_that!(snake)
67+
let expected_body = expected_body.to_vec();
68+
let expected_head = expected_body[0];
69+
70+
let failures = verify_that!(snake)
6871
.with_configured_diff_format()
69-
.extracting(|s| s.length)
70-
.named("snake.length")
72+
.extracting_ref("length", |s| &s.length)
7173
.is_equal_to(expected_body.len())
74+
.and()
75+
.extracting_ref("body", |s| &s.body)
76+
.contains_exactly(expected_body)
77+
.and()
78+
.extracting_ref("head", |s| &s.head)
79+
.is_equal_to(expected_head)
7280
.display_failures();
73-
failures.extend(
74-
verify_that!(snake)
75-
.with_configured_diff_format()
76-
.extracting(|s| &s.body)
77-
.named("snake.body")
78-
.contains_exactly(expected_body)
79-
.display_failures(),
80-
);
81-
failures.extend(
82-
verify_that!(snake)
83-
.with_configured_diff_format()
84-
.extracting(|s| s.head)
85-
.named("snake.head")
86-
.is_equal_to(expected_body[0])
87-
.display_failures(),
88-
);
81+
8982
assert!(
9083
failures.is_empty(),
9184
"assertion of snake body failed: \n\n{}",

examples/custom_assertion_reusing_existing.rs

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -68,34 +68,24 @@ where
6868
R: FailingStrategy,
6969
{
7070
fn has_body(mut self, expected_body: &[Coord]) -> Self {
71+
let expected_body = expected_body.to_vec();
72+
let expected_head = expected_body[0];
7173
let actual_body = self.subject().borrow();
7274
// we first collect all failures using the "soft assertion" mode of
7375
// asserting, which is started by using the `verify_that` function.
74-
let mut failures;
75-
failures = verify_that(actual_body)
76+
let failures = verify_that(actual_body)
7677
// `verify_that` does not highlight differences by default, so we
7778
// switch on highlighting using the configured `DiffFormat`
7879
.with_configured_diff_format()
79-
.extracting(|s| s.length)
80-
.named("snake.length")
80+
.extracting_ref("length", |s| &s.length)
8181
.is_equal_to(expected_body.len())
82-
.display_failures();
83-
failures.extend(
84-
verify_that(actual_body)
85-
.with_configured_diff_format()
86-
.extracting(|s| &s.body)
87-
.named("snake.body")
88-
.contains_exactly(expected_body)
89-
.display_failures(),
90-
);
91-
failures.extend(
92-
verify_that(actual_body)
93-
.with_configured_diff_format()
94-
.extracting(|s| s.head)
95-
.named("snake.head")
96-
.is_equal_to(expected_body[0])
97-
.display_failures(),
98-
);
82+
.and()
83+
.extracting_ref("body", |s| &s.body)
84+
.contains_exactly(expected_body)
85+
.and()
86+
.extracting_ref("head", |s| &s.head)
87+
.is_equal_to(expected_head)
88+
.display_failures();
9989
// if there are failures, we fail the whole assertion according to the
10090
// current `FailingStrategy`.
10191
if !failures.is_empty() {

src/derived_spec/mod.rs

Lines changed: 77 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,14 @@ impl<'a, O, S> DerivedSpec<'a, O, S> {
184184
/// method to switch back to the previous subject before calling
185185
/// `extracting_ref` for the other property.
186186
///
187+
/// The expression for failure reports is built from the expression of the
188+
/// original subject, a dot as a separator, and the given property name.
189+
/// The resulting expression is equal to
190+
/// `format!("{original_expression}.{property_name}")`.
191+
///
192+
/// If you do not like the default built expression, it can be overwritten
193+
/// by calling the `named` method after the call to the `extract` method.
194+
///
187195
/// # Arguments
188196
///
189197
/// * `property_name` - A name describing the extracted property used for
@@ -299,12 +307,14 @@ impl<'a, O, S> DerivedSpec<'a, O, S> {
299307
F: FnOnce(&S) -> &B,
300308
B: ToOwned<Owned = U> + ?Sized,
301309
{
302-
let extracted = extract(&self.subject).to_owned();
303-
let expression = Expression(property_name.into());
310+
let derived_subject = extract(&self.subject).to_owned();
311+
let orig_subject_name = &self.expression;
312+
let property_name = property_name.into();
313+
let expression = Expression(format!("{orig_subject_name}.{property_name}").into());
304314
let diff_format = self.diff_format.clone();
305315
DerivedSpec {
306316
original: self,
307-
subject: extracted,
317+
subject: derived_subject,
308318
expression,
309319
diff_format,
310320
}
@@ -330,15 +340,25 @@ impl<'a, O, S> DerivedSpec<'a, O, S> {
330340
/// that the "extracted" property is most likely a different subject than
331341
/// the original one.
332342
///
333-
/// It is recommended to give the extracted property a specific name by
334-
/// calling the `named` method. This helps with spotting the cause of a
335-
/// failing assertion.
343+
/// The expression for failure reports is built from the expression of the
344+
/// original subject, a dot as a separator, and the given property name.
345+
/// The resulting expression is equal to
346+
/// `format!("{original_expression}.{property_name}")`.
347+
///
348+
/// If you do not like the default built expression, it can be overwritten
349+
/// by calling the `named` method after the call to the `extract` method.
336350
///
337351
/// This method does not memorize the current subject. Calling `and` on the
338352
/// extracted property switches back to the original subject of this
339353
/// `DerivedSpec`. The current subject is omitted. So, `and` always switches
340354
/// back to the subject before the last `extracting_ref` call.
341355
///
356+
/// # Arguments
357+
///
358+
/// * `property_name` - A name describing the extracted property used for
359+
/// referencing this property in failure reports.
360+
/// * `extract` - A closure that returns the property to be extracted.
361+
///
342362
/// # Example
343363
///
344364
/// ```
@@ -374,26 +394,33 @@ impl<'a, O, S> DerivedSpec<'a, O, S> {
374394
///
375395
/// assert_that!(my_order)
376396
/// .extracting_ref("items", |o| &o.items)
377-
/// .extracting(|i| i[0].name.clone())
397+
/// .extracting("name", |i| i[0].name.clone())
378398
/// .is_equal_to("Apple")
379399
/// .and() // switches back to `my_order` not `my_order.items`
380-
/// .extracting(|o| o.id)
400+
/// .extracting("id", |o| o.id)
381401
/// .is_equal_to("O261234");
382402
/// ```
383403
///
384404
/// [`extracting_ref`]: Self::extracting_ref
385405
/// [`mapping`]: Self::mapping
386406
#[must_use = "a derived spec does nothing unless an assertion method is called"]
387-
pub fn extracting<F, U>(self, extract: F) -> DerivedSpec<'a, O, U>
407+
pub fn extracting<F, U>(
408+
self,
409+
property_name: impl Into<Cow<'a, str>>,
410+
extract: F,
411+
) -> DerivedSpec<'a, O, U>
388412
where
389413
F: FnOnce(S) -> U,
390414
{
391-
let extracted = extract(self.subject);
415+
let derived_subject = extract(self.subject);
416+
let orig_subject_name = &self.expression;
417+
let property_name = property_name.into();
418+
let expression = Expression(format!("{orig_subject_name}.{property_name}").into());
392419
let diff_format = self.diff_format.clone();
393420
DerivedSpec {
394421
original: self.original,
395-
subject: extracted,
396-
expression: Expression::default(),
422+
subject: derived_subject,
423+
expression,
397424
diff_format,
398425
}
399426
}
@@ -413,7 +440,8 @@ impl<'a, O, S> DerivedSpec<'a, O, S> {
413440
/// `DerivedSpec` also provides the [`extracting()`](DerivedSpec::extracting)
414441
/// method, which is similar to this method. In contrast to this method,
415442
/// [`extracting()`](DerivedSpec::extracting) does not copy the subject's
416-
/// name (or expression) but resets it to the default "subject".
443+
/// name (or expression) but builds a new expression from the expression of
444+
/// the original subject and the given property name.
417445
///
418446
/// # Example
419447
///
@@ -1567,7 +1595,10 @@ where
15671595
PanicOnFail.do_fail_with(&spec.failures());
15681596
unreachable!("Assertion failed and should have panicked! Please report a bug.")
15691597
}
1570-
spec.extracting(|mut collection| collection.remove(0))
1598+
let orig_subject_name = spec.expression();
1599+
let new_subject_name = format!("the first element of {orig_subject_name}");
1600+
spec.extracting("", |mut collection| collection.remove(0))
1601+
.named(new_subject_name)
15711602
}
15721603

15731604
fn last_element(self) -> Self::SingleElement {
@@ -1578,11 +1609,14 @@ where
15781609
PanicOnFail.do_fail_with(&spec.failures());
15791610
unreachable!("Assertion failed and should have panicked! Please report a bug.")
15801611
}
1581-
spec.extracting(|mut collection| {
1612+
let orig_subject_name = spec.expression();
1613+
let new_subject_name = format!("the last element of {orig_subject_name}");
1614+
spec.extracting("", |mut collection| {
15821615
collection.pop().unwrap_or_else(|| {
15831616
unreachable!("Assertion failed and should have panicked! Please report a bug.")
15841617
})
15851618
})
1619+
.named(new_subject_name)
15861620
}
15871621

15881622
fn nth_element(self, n: usize) -> Self::SingleElement {
@@ -1594,10 +1628,16 @@ where
15941628
PanicOnFail.do_fail_with(&spec.failures());
15951629
unreachable!("Assertion failed and should have panicked! Please report a bug.")
15961630
}
1597-
spec.extracting(|mut collection| collection.remove(n))
1631+
let orig_subject_name = spec.expression();
1632+
let new_subject_name = format!("{orig_subject_name}[{n}]");
1633+
spec.extracting("", |mut collection| collection.remove(n))
1634+
.named(new_subject_name)
15981635
}
15991636

16001637
fn elements_at(self, indices: impl IntoIterator<Item = usize>) -> Self::MultipleElements {
1638+
let indices = Vec::from_iter(indices);
1639+
let orig_subject_name = self.expression();
1640+
let new_subject_name = format!("{orig_subject_name} at positions {indices:?}");
16011641
let indices = HashSet::<_>::from_iter(indices);
16021642
self.mapping(|subject| {
16031643
subject
@@ -1606,6 +1646,7 @@ where
16061646
.filter_map(|(i, v)| if indices.contains(&i) { Some(v) } else { None })
16071647
.collect()
16081648
})
1649+
.named(new_subject_name)
16091650
}
16101651
}
16111652

@@ -1706,12 +1747,12 @@ where
17061747
}
17071748
let orig_subject_name = original_spec.expression();
17081749
let new_subject_name = format!("the first element of {orig_subject_name}");
1709-
original_spec.extracting_ref(new_subject_name, |collection|
1750+
original_spec.extracting_ref("", |collection|
17101751
collection.first()
17111752
.unwrap_or_else(||
17121753
unreachable!("We should have asserted before, that there is at least one element in the collection/iterator. Please file a bug.")
17131754
)
1714-
)
1755+
).named(new_subject_name)
17151756
}
17161757

17171758
fn last_element_ref(self) -> Self::SingleElement {
@@ -1724,12 +1765,12 @@ where
17241765
}
17251766
let orig_subject_name = original_spec.expression();
17261767
let new_subject_name = format!("the last element of {orig_subject_name}");
1727-
original_spec.extracting_ref(new_subject_name, |collection|
1768+
original_spec.extracting_ref("", |collection|
17281769
collection.last()
17291770
.unwrap_or_else(||
17301771
unreachable!("We should have asserted before, that there is at least one element in the collection/iterator. Please file a bug.")
17311772
)
1732-
)
1773+
).named(new_subject_name)
17331774
}
17341775

17351776
fn nth_element_ref(self, n: usize) -> Self::SingleElement {
@@ -1743,12 +1784,12 @@ where
17431784
}
17441785
let orig_subject_name = original_spec.expression();
17451786
let new_subject_name = format!("{orig_subject_name}[{n}]");
1746-
original_spec.extracting_ref(new_subject_name, |collection|
1787+
original_spec.extracting_ref("", |collection|
17471788
collection.get(n)
17481789
.unwrap_or_else(||
17491790
unreachable!("We should have asserted before, that there is at least one element in the collection/iterator. Please file a bug.")
17501791
)
1751-
)
1792+
).named(new_subject_name)
17521793
}
17531794

17541795
fn elements_ref_at(self, indices: impl IntoIterator<Item = usize>) -> Self::MultipleElements {
@@ -1757,18 +1798,20 @@ where
17571798
let new_subject_name = format!("{orig_subject_name} at positions {indices:?}");
17581799
let indices = HashSet::<_>::from_iter(indices);
17591800
let original_spec = self.mapping(Vec::from_iter);
1760-
original_spec.extracting_ref_iter(new_subject_name, |collection| {
1761-
collection
1762-
.enumerate()
1763-
.filter_map(|(i, e)| {
1764-
if indices.contains(&i) {
1765-
Some(e.to_owned())
1766-
} else {
1767-
None
1768-
}
1769-
})
1770-
.collect()
1771-
})
1801+
original_spec
1802+
.extracting_ref_iter("", |collection| {
1803+
collection
1804+
.enumerate()
1805+
.filter_map(|(i, e)| {
1806+
if indices.contains(&i) {
1807+
Some(e.to_owned())
1808+
} else {
1809+
None
1810+
}
1811+
})
1812+
.collect()
1813+
})
1814+
.named(new_subject_name)
17721815
}
17731816
}
17741817

src/derived_spec/tests.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ fn extracting_person_name_contains_i() {
6666
gender: Gender::Female,
6767
};
6868

69-
assert_that(person).extracting(|p| p.name).contains('i');
69+
assert_that(person)
70+
.extracting("name", |p| p.name)
71+
.contains('i');
7072
}
7173

7274
#[test]

src/iterator/mod.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -800,11 +800,14 @@ where
800800
PanicOnFail.do_fail_with(&spec.failures());
801801
unreachable!("Assertion failed and should have panicked! Please report a bug.")
802802
}
803-
spec.extracting(|mut collection| {
803+
let original_expression = spec.expression();
804+
let new_expression = format!("{original_expression}'s only element");
805+
spec.extracting("", |mut collection| {
804806
collection.pop().unwrap_or_else(|| {
805807
unreachable!("Assertion failed and should have panicked! Please report a bug.")
806808
})
807809
})
810+
.named(new_expression)
808811
}
809812

810813
fn filtered_on<C>(self, condition: C) -> Self::MultipleElements
@@ -975,7 +978,10 @@ where
975978
PanicOnFail.do_fail_with(&spec.failures());
976979
unreachable!("Assertion failed and should have panicked! Please report a bug.")
977980
}
978-
spec.extracting(|mut collection| collection.remove(0))
981+
let orig_subject_name = spec.expression();
982+
let new_subject_name = format!("the first element of {orig_subject_name}");
983+
spec.extracting("", |mut collection| collection.remove(0))
984+
.named(new_subject_name)
979985
}
980986

981987
fn last_element(self) -> Self::SingleElement {
@@ -986,11 +992,14 @@ where
986992
PanicOnFail.do_fail_with(&spec.failures());
987993
unreachable!("Assertion failed and should have panicked! Please report a bug.")
988994
}
989-
spec.extracting(|mut collection| {
995+
let orig_subject_name = spec.expression();
996+
let new_subject_name = format!("the last element of {orig_subject_name}");
997+
spec.extracting("", |mut collection| {
990998
collection.pop().unwrap_or_else(|| {
991999
unreachable!("Assertion failed and should have panicked! Please report a bug.")
9921000
})
9931001
})
1002+
.named(new_subject_name)
9941003
}
9951004

9961005
fn nth_element(self, n: usize) -> Self::SingleElement {
@@ -1002,7 +1011,10 @@ where
10021011
PanicOnFail.do_fail_with(&spec.failures());
10031012
unreachable!("Assertion failed and should have panicked! Please report a bug.")
10041013
}
1005-
spec.extracting(|mut collection| collection.remove(n))
1014+
let orig_subject_name = spec.expression();
1015+
let new_subject_name = format!("{orig_subject_name}[{n}]");
1016+
spec.extracting("", |mut collection| collection.remove(n))
1017+
.named(new_subject_name)
10061018
}
10071019

10081020
fn elements_at(self, indices: impl IntoIterator<Item = usize>) -> Self::MultipleElements {

0 commit comments

Comments
 (0)