Skip to content

Commit

Permalink
Change map function to chain. Add map for assertions on collect…
Browse files Browse the repository at this point in the history
…ions (#111)

Change `map` to `chain` and add new `map` function for iterable subjects

Fixes #103
  • Loading branch information
robfletcher authored Sep 23, 2018
1 parent 62a46e4 commit ee43713
Show file tree
Hide file tree
Showing 28 changed files with 141 additions and 98 deletions.
1 change: 1 addition & 0 deletions site/src/jbake/content/about.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ cached=true
Strikt was written by [Rob Fletcher](https://github.com/robfletcher) with contributions from:
* [Casey Brooks](https://github.com/cjbrooks12)
* [Xavier Hanin](https://github.com/xhanin)
* [Paul Merlin](https://github.com/eskatos)
* [Christoph Sturm](https://github.com/christophsturm)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
title=Mapping Over Assertion Subjects
title=Traversing Assertion Subjects
type=page
status=published
cached=true
previousPage=flow-typing.html
nextPage=grouping-with-and.html
~~~~~~
# Mapping Over Assertion Subjects
# Traversing Assertion Subjects
Although you can obviously write assertions for the properties of an object with code like this:
Expand All @@ -19,9 +19,9 @@ expectThat(person.name).isEqualTo("Ziggy")
Sometimes it's useful to be able to transform an assertion on a subject to an assertion on a property of that subject, or the result of a method call.
Particularly when using soft assertion blocks.
Strikt allows for this using the `Assertion.Builder<T>.map` method.
Strikt allows for this using the `Assertion.Builder<T>.chain` method.
## Mapping with lambdas
## Using _chain_ with lambdas
The method takes a lambda whose parameter is the current subject and returns an `Assertion.Builder<R>` where `R` is the type of whatever the lambda returns.
Expand All @@ -30,43 +30,58 @@ This is sometimes useful for making assertions about the properties of an object
```kotlin
val subject = Person(name = "David", birthDate = LocalDate.of(1947, 1, 8))
expectThat(subject) {
map { it.name }.isEqualTo("David")
map { it.birthDate.year }.isEqualTo(1947)
chain { it.name }.isEqualTo("David")
chain { it.birthDate.year }.isEqualTo(1947)
}
```
Strikt will read the test source to find out the name of the variables, so this will work just as well as property references and produce output that looks like this:
Strikt will read the test source to find out the name of the variables.
This example produces output that looks like this:
```
▼ name:
✗ is equal to "Ziggy" : found "David"
▼ birthDate.year:
✗ is equal to 1971 : found 1947
```
## Mapping with property or method references
## Using _chain_ with property or method references
It's also possible to use a method reference in place of a lambda.
```kotlin
val subject = Person(name = "David", birthDate = LocalDate.of(1947, 1, 8))
expectThat(subject) {
map(Person::name).isEqualTo("David")
map(Person::birthDate).map(LocalDate::getYear).isEqualTo(1947)
chain(Person::name).isEqualTo("David")
chain(Person::birthDate).map(LocalDate::getYear).isEqualTo(1947)
}
```
## Mapping elements of collections
If the assertion subject is an `Iterable` Strikt provides a `map` function much like the one in the Kotlin standard library.
It is effectively like using `chain` on each element of the `Iterable` subject.
```kotlin
val subject: List<Person> = // get list from somewhere
expectThat(subject)
.map(Person::name)
.containsExactly("David", "Ziggy", "Aladdin", "Jareth")
```
In this case the `map` function is transforming the `Assertion.Buidler<List<Person>>` into an `Assertion.Builder<List<String>>` by applying the `name` property to each element.
## Re-usable mappings
If you find yourself frequently using `map` for the same properties or methods, consider defining extension property or method to make things even easier.
If you find yourself frequently using `chain` for the same properties or methods, consider defining extension property or method to make things even easier.
For example:
```kotlin
val Assertion.Builder<Person>.name: Assertion.Builder<String>
get() = map(Person::name)
get() = chain(Person::name)
val Assertion.Builder<Person>.yearOfBirth: Assertion.Builder<LocalDate>
get() = map("year of birth") { it.dateOfBirth.year }
get() = chain("year of birth") { it.dateOfBirth.year }
```
You can then write the earlier example as:
Expand All @@ -79,7 +94,7 @@ expectThat(subject) {
}
```
## Built-in mappings
## Built-in traversals
Strikt has a number of built in mapping properties and functions such as `Assertion.Builder<List<E>>.first()` which returns an `Assertion.Builder<E>` whose subject is the first element of the list.
Strikt has a number of built in traversal properties and functions such as `Assertion.Builder<List<E>>.first()` which returns an `Assertion.Builder<E>` whose subject is the first element of the list.
See the [API docs](/api/strikt-core/strikt.api/-assertion/) for details.
3 changes: 2 additions & 1 deletion site/src/jbake/templates/navbar.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
<a class="navbar-item" href="/user-guide/expecting-exceptions.html">Expecting
Exceptions</a>
<a class="navbar-item" href="/user-guide/flow-typing.html">Strongly Typed Assertions</a>
<a class="navbar-item" href="/user-guide/mapping.html">Mapping Over Assertion Subjects</a>
<a class="navbar-item" href="/user-guide/traversing-subjects.html">Traversing
Assertion Subjects</a>
<a class="navbar-item" href="/user-guide/grouping-with-and.html">Grouping Assertions with and</a>
<a class="navbar-item" href="/user-guide/custom-assertions.html">Custom
Assertions</a>
Expand Down
19 changes: 12 additions & 7 deletions strikt-core/src/main/kotlin/strikt/api/Assertion.kt
Original file line number Diff line number Diff line change
Expand Up @@ -168,18 +168,20 @@ interface Assertion {
* reference) the subject description will be automatically determined for
* the returned assertion builder.
*
* If [function] is a lambda, Strikt will make a best-effort attempt to
* determine an appropriate function / property name.
*
* @param function a lambda whose receiver is the current assertion subject.
* @return an assertion builder whose subject is the value returned by
* [function].
*/
// TODO: not sure about this name, it's fundamentally similar to Kotlin's run. Also it might be nice to have a dedicated `map` for Assertion<Iterable>.
fun <R> map(function: (T) -> R): DescribeableBuilder<R> =
fun <R> chain(function: (T) -> R): DescribeableBuilder<R> =
when (function) {
is KProperty<*> ->
map("value of property ${function.name}", function)
chain("value of property ${function.name}", function)
is KFunction<*> ->
map("return value of ${function.name}", function)
is CallableReference -> map(
chain("return value of ${function.name}", function)
is CallableReference -> chain(
"value of ${function.propertyName}",
function
)
Expand All @@ -190,7 +192,7 @@ interface Assertion {
} catch (e: Exception) {
"%s"
}
map(fieldName, function)
chain(fieldName, function)
}
}

Expand All @@ -204,7 +206,10 @@ interface Assertion {
* @return an assertion builder whose subject is the value returned by
* [function].
*/
fun <R> map(description: String, function: (T) -> R): DescribeableBuilder<R>
fun <R> chain(
description: String,
function: (T) -> R
): DescribeableBuilder<R>

/**
* Reverses any assertions chained after this method.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import strikt.api.Assertion.Builder
* assertion subject.
*
* Since it doesn't make sense to do this anywhere except directly after the
* initial [expectThat] or [Assertion.Builder.map] call those methods return an
* initial [expectThat] or [Assertion.Builder.chain] call those methods return an
* instance of this interface, while assertions themselves just return
* [Assertion.Builder].
*/
Expand Down
2 changes: 1 addition & 1 deletion strikt-core/src/main/kotlin/strikt/assertions/Any.kt
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ fun <T : Any> Builder<T>.propertiesAreEqualTo(other: T): Builder<T> =
.propertyDescriptors
.filter { it.name != "class" }
.forEach { property ->
val mappedAssertion = map("value of property ${property.name}") {
val mappedAssertion = chain("value of property ${property.name}") {
property.readMethod(it)
}
val otherValue = property.readMethod(other)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,4 @@ fun <T : CharSequence> Builder<T>.containsIgnoringCase(expected: CharSequence):
* @see CharSequence.length
*/
val <T : CharSequence> Builder<T>.length: Builder<Int>
get() = map(CharSequence::length)
get() = chain(CharSequence::length)
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ fun <T : Collection<E>, E> Builder<T>.isNotEmpty(): Builder<T> =
* @see Collection.size
*/
val <T : Collection<*>> Builder<T>.size: Builder<Int>
get() = map(Collection<*>::size)
get() = chain(Collection<*>::size)
4 changes: 2 additions & 2 deletions strikt-core/src/main/kotlin/strikt/assertions/Enum.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import strikt.api.Assertion.Builder
* @see Enum.name
*/
val <T : Enum<T>> Builder<T>.name: Builder<String>
get() = map(Enum<T>::name)
get() = chain(Enum<T>::name)

/**
* Maps an assertion on an enum to an assertion on its ordinal.
*
* @see Enum.ordinal
*/
val <T : Enum<T>> Builder<T>.ordinal: Builder<Int>
get() = map(Enum<T>::ordinal)
get() = chain(Enum<T>::ordinal)
17 changes: 12 additions & 5 deletions strikt-core/src/main/kotlin/strikt/assertions/Iterable.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,21 @@ package strikt.assertions

import strikt.api.Assertion.Builder

/**
* Applies [Iterable.map] with [function] to the subject and returns an
* assertion builder wrapping the result.
*/
fun <T : Iterable<E>, E, R> Builder<T>.map(function: (E) -> R): Builder<Iterable<R>> =
chain { it.map(function) }

/**
* Maps this assertion to an assertion over the first element in the subject
* iterable.
*
* @see Iterable.first
*/
fun <T : Iterable<E>, E> Builder<T>.first(): Builder<E> =
map("first element %s") { it.first() }
chain("first element %s") { it.first() }

/**
* Maps this assertion to an assertion over the last element in the subject
Expand All @@ -18,15 +25,15 @@ fun <T : Iterable<E>, E> Builder<T>.first(): Builder<E> =
* @see Iterable.last
*/
fun <T : Iterable<E>, E> Builder<T>.last(): Builder<E> =
map("last element %s") { it.last() }
chain("last element %s") { it.last() }

/**
* Asserts that all elements of the subject pass the assertions in [predicate].
*/
fun <T : Iterable<E>, E> Builder<T>.all(predicate: Builder<E>.() -> Unit): Builder<T> =
compose("all elements match:") { subject ->
subject.forEach { element ->
map("%s") { element }.apply(predicate)
chain("%s") { element }.apply(predicate)
}
} then {
if (allPassed) pass() else fail()
Expand All @@ -39,7 +46,7 @@ fun <T : Iterable<E>, E> Builder<T>.all(predicate: Builder<E>.() -> Unit): Build
fun <T : Iterable<E>, E> Builder<T>.any(predicate: Builder<E>.() -> Unit): Builder<T> =
compose("at least one element matches:") { subject ->
subject.forEach { element ->
map("%s") { element }.apply(predicate)
chain("%s") { element }.apply(predicate)
}
} then {
if (anyPassed) pass() else fail()
Expand All @@ -51,7 +58,7 @@ fun <T : Iterable<E>, E> Builder<T>.any(predicate: Builder<E>.() -> Unit): Build
fun <T : Iterable<E>, E> Builder<T>.none(predicate: Builder<E>.() -> Unit): Builder<T> =
compose("no elements match:") { subject ->
subject.forEach { element ->
map("%s") { element }.apply(predicate)
chain("%s") { element }.apply(predicate)
}
} then {
if (allFailed) pass() else fail()
Expand Down
4 changes: 2 additions & 2 deletions strikt-core/src/main/kotlin/strikt/assertions/List.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import strikt.api.Assertion.Builder
* subject list.
*/
operator fun <T : List<E>, E> Builder<T>.get(i: Int): Builder<E> =
map("element [$i] %s") { it[i] }
chain("element [$i] %s") { it[i] }

/**
* Maps this assertion to an assertion on the elements at the sub-list
* represented by [range] in the subject list.
*/
operator fun <T : List<E>, E> Builder<T>.get(range: IntRange): Builder<List<E>> =
map("elements [$range] %s") {
chain("elements [$range] %s") {
it.subList(range.first, range.last + 1)
}
4 changes: 2 additions & 2 deletions strikt-core/src/main/kotlin/strikt/assertions/Map.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ fun <T : Map<K, V>, K, V> Builder<T>.isEmpty() =
* exists in the subject map.
*/
operator fun <T : Map<K, V>, K, V> Builder<T>.get(key: K): Builder<V?> =
map("entry [${formatValue(key)}]") { it[key] }
chain("entry [${formatValue(key)}]") { it[key] }

/**
* Asserts that the subject map contains an entry indexed by [key]. Depending on
Expand All @@ -34,7 +34,7 @@ fun <T : Map<K, V>, K, V> Builder<T>.containsKey(key: K): Builder<T> =
*/
fun <T : Map<K, V>, K, V> Builder<T>.containsKeys(vararg keys: K): Builder<T> =
compose("has entries with the keys %s", keys.toList()) {
keys.forEach { containsKey(it) }
keys.forEach { key -> containsKey(key) }
} then {
if (allPassed) pass() else fail()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ fun <T : TemporalAccessor> Assertion.Builder<T>.isAfter(expected: TemporalAccess
* @see TemporalAccessor.get
*/
fun <T : TemporalAccessor> Assertion.Builder<T>.get(field: TemporalField): Assertion.Builder<Int> =
map(field.toString()) { it.get(field) }
chain(field.toString()) { it.get(field) }

/**
* Maps an assertion on the subject to an assertion on the value of the
Expand All @@ -74,4 +74,4 @@ fun <T : TemporalAccessor> Assertion.Builder<T>.get(field: TemporalField): Asser
* @see TemporalAccessor.getLong
*/
fun <T : TemporalAccessor> Assertion.Builder<T>.getLong(field: TemporalField): Assertion.Builder<Long> =
map(field.toString()) { it.getLong(field) }
chain(field.toString()) { it.getLong(field) }
5 changes: 3 additions & 2 deletions strikt-core/src/main/kotlin/strikt/assertions/Throwable.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,21 @@ import strikt.api.DescribeableBuilder
* @author [Xavier Hanin](https://github.com/xhanin)
*/
val <T : Throwable> Builder<T>.message: Builder<String>
get() = map(Throwable::message).isNotNull()
get() = chain(Throwable::message).isNotNull()

/**
* Maps an assertion on a [Throwable] to an assertion on its [Throwable.cause].
*/
val <T : Throwable> Builder<T>.cause: DescribeableBuilder<Throwable?>
get() = map(Throwable::cause)
get() = chain(Throwable::cause)

/**
* Asserts that an exception is an instance of the expected type.
* The assertion fails if the subject is `null` or not an instance of [E].
*
* This assertion is designed for use with the [strikt.api.catching] function.
*/
@Suppress("UNCHECKED_CAST")
inline fun <reified E : Throwable> Builder<Throwable?>.throws(): Builder<E> =
assert("threw %s", E::class.java) {
when (it) {
Expand Down
10 changes: 5 additions & 5 deletions strikt-core/src/main/kotlin/strikt/assertions/Tuples.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,31 @@ import strikt.api.Assertion
*/
@get:JvmName("first_pair")
val <A, B> Assertion.Builder<Pair<A, B>>.first: Assertion.Builder<A>
get() = map(Pair<A, B>::first)
get() = chain(Pair<A, B>::first)

/**
* Maps an assertion on a [Pair] to an assertion on its [Pair.second] property.
*/
@get:JvmName("second_pair")
val <A, B> Assertion.Builder<Pair<A, B>>.second: Assertion.Builder<B>
get() = map(Pair<A, B>::second)
get() = chain(Pair<A, B>::second)

/**
* Maps an assertion on a [Triple] to an assertion on its [Triple.first] property.
*/
@get:JvmName("first_triple")
val <A, B, C> Assertion.Builder<Triple<A, B, C>>.first: Assertion.Builder<A>
get() = map(Triple<A, B, C>::first)
get() = chain(Triple<A, B, C>::first)

/**
* Maps an assertion on a [Triple] to an assertion on its [Triple.second] property.
*/
@get:JvmName("second_triple")
val <A, B, C> Assertion.Builder<Triple<A, B, C>>.second: Assertion.Builder<B>
get() = map(Triple<A, B, C>::second)
get() = chain(Triple<A, B, C>::second)

/**
* Maps an assertion on a [Triple] to an assertion on its [Triple.third] property.
*/
val <A, B, C> Assertion.Builder<Triple<A, B, C>>.third: Assertion.Builder<C>
get() = map(Triple<A, B, C>::third)
get() = chain(Triple<A, B, C>::third)
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ internal class AssertionBuilder<T>(
}
}

override fun <R> map(
override fun <R> chain(
description: String,
function: (T) -> R
): DescribeableBuilder<R> =
Expand Down
Loading

0 comments on commit ee43713

Please sign in to comment.