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`. return type of your function a `Result`.
```kotlin ```kotlin
fun findById(id: Int): Result<Customer, DatabaseError> { fun checkPrivileges(user: User, command: Command): Result<Command, CommandError> {
val customer = getAllCustomers().find { it.id == id } return if (user.rank >= command.mininimumRank) {
return if (customer != null) Ok(customer) else Err(DatabaseError.CustomerNotFound) 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 ### Transforming Results
Both success and failure results can be transformed within a stage of the 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 ```kotlin
val result: Result<Treasure, UnlockResponse> = val result: Result<Treasure, UnlockResponse> =
unlockVault("my-password") // returns Result<Treasure, UnlockError> unlockVault("my-password") // returns Result<Treasure, UnlockError>
.mapError { UnlockResponse.IncorrectPassword } // transform UnlockError into UnlockResponse.IncorrectPassword .mapError { IncorrectPassword } // transform UnlockError into IncorrectPassword
``` ```
### Chaining ### 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 happy path for a user entering commands into an administrative console would
consist of: the command being tokenized, the command being registered, the user 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 ```kotlin
tokenize(command.toLowerCase()) 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
Inspiration for this library has been drawn from other languages in which the 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 package com.github.michaelbull.result.example
import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result import com.github.michaelbull.result.Result
import com.github.michaelbull.result.andThen 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.example.service.CustomerService
import com.github.michaelbull.result.mapBoth import com.github.michaelbull.result.mapBoth
import com.github.michaelbull.result.mapError import com.github.michaelbull.result.mapError
import com.github.michaelbull.result.toResultOr
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.call import io.ktor.application.call
import io.ktor.application.install import io.ktor.application.install
@ -91,8 +91,9 @@ fun Application.main() {
} }
private fun readId(values: ValuesMap): Result<Long, DomainMessage> { private fun readId(values: ValuesMap): Result<Long, DomainMessage> {
val id = values["id"]?.toLongOrNull() return values["id"]
return if (id != null) Ok(id) else Err(CustomerRequired) ?.toLongOrNull()
.toResultOr { CustomerRequired }
} }
private fun messageToResponse(message: DomainMessage) = when (message) { 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.map
import com.github.michaelbull.result.mapBoth import com.github.michaelbull.result.mapBoth
import com.github.michaelbull.result.mapError import com.github.michaelbull.result.mapError
import com.github.michaelbull.result.toResultOr
import java.sql.SQLTimeoutException import java.sql.SQLTimeoutException
object CustomerService { object CustomerService {
@ -36,7 +37,7 @@ object CustomerService {
} }
fun getById(id: CustomerId): Result<Customer, DomainMessage> { 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> { fun upsert(customer: Customer): Result<DomainMessage?, DomainMessage> {
@ -57,9 +58,8 @@ object CustomerService {
.map { CustomerCreated } .map { CustomerCreated }
.mapError(::exceptionToDomainMessage) .mapError(::exceptionToDomainMessage)
private fun findCustomer(customers: Collection<Customer>, id: CustomerId): Result<Customer, CustomerNotFound> { private fun Collection<Customer>.findCustomer(id: CustomerId): Result<Customer, CustomerNotFound> {
val customer = customers.find { it.id == id } return find { it.id == id }.toResultOr { CustomerNotFound }
return if (customer != null) Ok(customer) else Err(CustomerNotFound)
} }
private fun differenceBetween(old: Customer, new: Customer): EmailAddressChanged? { private fun differenceBetween(old: Customer, new: Customer): EmailAddressChanged? {

View File

@ -11,10 +11,10 @@ sealed class Result<out V, out E> {
companion object { companion object {
/** /**
* Invokes a [function] and wraps it in a [Result], returning an [Err] if an [Exception] * Invokes a [function] and wraps it in a [Result], returning an [Err]
* was thrown, otherwise [Ok]. * 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 { return try {
Ok(function.invoke()) Ok(function.invoke())
} catch (ex: Exception) { } 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]. * Represents a failed [Result], containing an [error].
*/ */
data class Err<out E>(val error: E) : Result<Nothing, E>() 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 @Test
internal fun returnsOkIfInvocationSuccessful() { internal fun returnsOkIfInvocationSuccessful() {
val callback = { "example" } val callback = { "example" }
val result = Result.of(callback)
assertEquals( assertEquals(
expected = "example", expected = "example",
actual = Result.of(callback).get() actual = result.get()
) )
} }
@ -20,11 +21,33 @@ internal class ResultTest {
internal fun returnsErrIfInvocationFails() { internal fun returnsErrIfInvocationFails() {
val exception = IllegalArgumentException("throw me") val exception = IllegalArgumentException("throw me")
val callback = { throw exception } val callback = { throw exception }
val error = Result.of(callback).getError()!! val result = Result.of(callback)
assertSame( assertSame(
expected = exception, 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()
) )
} }
} }