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,10 +52,22 @@ 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
@ -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
@ -77,6 +89,7 @@ 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.
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()
)
}
}