Add toResultOr

Acts as a factory function to convert nullable types to Result types
This commit is contained in:
Michael Bull 2018-01-24 18:20:39 +00:00
parent c93ac7fb18
commit 410563b621
5 changed files with 66 additions and 29 deletions

View File

@ -52,12 +52,24 @@ Oriented Programming is to avoid throwing an exception and instead make the
return type of your function a `Result`.
```kotlin
fun findById(id: Int): Result<Customer, DatabaseError> {
val customer = getAllCustomers().find { it.id == id }
return if (customer != null) Ok(customer) else Err(DatabaseError.CustomerNotFound)
fun checkPrivileges(user: User, command: Command): Result<Command, CommandError> {
return if (user.rank >= command.mininimumRank) {
Ok(command)
} else {
Err(CommandError.InsufficientRank(command.name))
}
}
```
Nullable types, such as the `find` method in the example below, can be
converted to a `Result` using the `toResultOr` extension function.
```kotlin
val result: Result<Customer, String> = customers
.find { it.id == id } // returns Customer?
.toResultOr { "No customer found" }
```
### Transforming Results
Both success and failure results can be transformed within a stage of the
@ -68,7 +80,7 @@ program error (`UnlockError`) into an exposed client error
```kotlin
val result: Result<Treasure, UnlockResponse> =
unlockVault("my-password") // returns Result<Treasure, UnlockError>
.mapError { UnlockResponse.IncorrectPassword } // transform UnlockError into UnlockResponse.IncorrectPassword
.mapError { IncorrectPassword } // transform UnlockError into IncorrectPassword
```
### Chaining
@ -76,7 +88,8 @@ val result: Result<Treasure, UnlockResponse> =
Results can be chained to produce a "happy path" of execution. For example, the
happy path for a user entering commands into an administrative console would
consist of: the command being tokenized, the command being registered, the user
having sufficient privileges, and the command executing the associated action.
having sufficient privileges, and the command executing the associated action.
The example below uses the `checkPrivileges` function we defined earlier.
```kotlin
tokenize(command.toLowerCase())
@ -89,17 +102,6 @@ tokenize(command.toLowerCase())
)
```
Each of the `andThen` steps produces its own result, for example:
```kotlin
fun checkPrivileges(user: User, command: TokenizedCommand): Result<Command, CommandError> {
return when {
user.rank >= command.minRank -> Ok(command)
else -> Err(CommandError.InsufficientRank(command.tokens.name))
}
}
```
## Inspiration
Inspiration for this library has been drawn from other languages in which the

View File

@ -1,6 +1,5 @@
package com.github.michaelbull.result.example
import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
import com.github.michaelbull.result.andThen
@ -26,6 +25,7 @@ import com.github.michaelbull.result.example.model.dto.CustomerDto
import com.github.michaelbull.result.example.service.CustomerService
import com.github.michaelbull.result.mapBoth
import com.github.michaelbull.result.mapError
import com.github.michaelbull.result.toResultOr
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
@ -91,8 +91,9 @@ fun Application.main() {
}
private fun readId(values: ValuesMap): Result<Long, DomainMessage> {
val id = values["id"]?.toLongOrNull()
return if (id != null) Ok(id) else Err(CustomerRequired)
return values["id"]
?.toLongOrNull()
.toResultOr { CustomerRequired }
}
private fun messageToResponse(message: DomainMessage) = when (message) {

View File

@ -16,6 +16,7 @@ import com.github.michaelbull.result.example.model.entity.CustomerEntity
import com.github.michaelbull.result.map
import com.github.michaelbull.result.mapBoth
import com.github.michaelbull.result.mapError
import com.github.michaelbull.result.toResultOr
import java.sql.SQLTimeoutException
object CustomerService {
@ -36,7 +37,7 @@ object CustomerService {
}
fun getById(id: CustomerId): Result<Customer, DomainMessage> {
return getAll().andThen { findCustomer(it, id) }
return getAll().andThen { it.findCustomer(id) }
}
fun upsert(customer: Customer): Result<DomainMessage?, DomainMessage> {
@ -57,9 +58,8 @@ object CustomerService {
.map { CustomerCreated }
.mapError(::exceptionToDomainMessage)
private fun findCustomer(customers: Collection<Customer>, id: CustomerId): Result<Customer, CustomerNotFound> {
val customer = customers.find { it.id == id }
return if (customer != null) Ok(customer) else Err(CustomerNotFound)
private fun Collection<Customer>.findCustomer(id: CustomerId): Result<Customer, CustomerNotFound> {
return find { it.id == id }.toResultOr { CustomerNotFound }
}
private fun differenceBetween(old: Customer, new: Customer): EmailAddressChanged? {

View File

@ -11,10 +11,10 @@ sealed class Result<out V, out E> {
companion object {
/**
* Invokes a [function] and wraps it in a [Result], returning an [Err] if an [Exception]
* was thrown, otherwise [Ok].
* Invokes a [function] and wraps it in a [Result], returning an [Err]
* if an [Exception] was thrown, otherwise [Ok].
*/
inline fun <T> of(function: () -> T): Result<T, Exception> {
inline fun <V> of(function: () -> V): Result<V, Exception> {
return try {
Ok(function.invoke())
} catch (ex: Exception) {
@ -33,3 +33,14 @@ data class Ok<out V>(val value: V) : Result<V, Nothing>()
* Represents a failed [Result], containing an [error].
*/
data class Err<out E>(val error: E) : Result<Nothing, E>()
/**
* Converts a nullable of type [V] to a [Result]. Returns [Ok] if the value is
* non-null, otherwise the supplied [error].
*/
inline infix fun <V, E> V?.toResultOr(error: () -> E): Result<V, E> {
return when (this) {
null -> Err(error())
else -> Ok(this)
}
}

View File

@ -9,10 +9,11 @@ internal class ResultTest {
@Test
internal fun returnsOkIfInvocationSuccessful() {
val callback = { "example" }
val result = Result.of(callback)
assertEquals(
expected = "example",
actual = Result.of(callback).get()
actual = result.get()
)
}
@ -20,11 +21,33 @@ internal class ResultTest {
internal fun returnsErrIfInvocationFails() {
val exception = IllegalArgumentException("throw me")
val callback = { throw exception }
val error = Result.of(callback).getError()!!
val result = Result.of(callback)
assertSame(
expected = exception,
actual = error
actual = result.getError()
)
}
}
internal class `toResultOr` {
@Test
internal fun returnsOkfIfNonNull() {
val result = "ok".toResultOr { "err" }
assertEquals(
expected = "ok",
actual = result.get()
)
}
@Test
internal fun returnsErrIfNull() {
val result = "ok".toLongOrNull().toResultOr { "err" }
assertEquals(
expected = "err",
actual = result.getError()
)
}
}