diff --git a/README.md b/README.md index 6db4c0d..b5a1639 100644 --- a/README.md +++ b/README.md @@ -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 { - val customer = getAllCustomers().find { it.id == id } - return if (customer != null) Ok(customer) else Err(DatabaseError.CustomerNotFound) +fun checkPrivileges(user: User, command: Command): Result { + 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 = 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 = unlockVault("my-password") // returns Result - .mapError { UnlockResponse.IncorrectPassword } // transform UnlockError into UnlockResponse.IncorrectPassword + .mapError { IncorrectPassword } // transform UnlockError into IncorrectPassword ``` ### Chaining @@ -76,7 +88,8 @@ val result: Result = 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 { - 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 diff --git a/example/src/main/kotlin/com/github/michaelbull/result/example/Application.kt b/example/src/main/kotlin/com/github/michaelbull/result/example/Application.kt index 211697b..578cf24 100644 --- a/example/src/main/kotlin/com/github/michaelbull/result/example/Application.kt +++ b/example/src/main/kotlin/com/github/michaelbull/result/example/Application.kt @@ -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 { - 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) { diff --git a/example/src/main/kotlin/com/github/michaelbull/result/example/service/CustomerService.kt b/example/src/main/kotlin/com/github/michaelbull/result/example/service/CustomerService.kt index 6b7af24..480c3b7 100644 --- a/example/src/main/kotlin/com/github/michaelbull/result/example/service/CustomerService.kt +++ b/example/src/main/kotlin/com/github/michaelbull/result/example/service/CustomerService.kt @@ -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 { - return getAll().andThen { findCustomer(it, id) } + return getAll().andThen { it.findCustomer(id) } } fun upsert(customer: Customer): Result { @@ -57,9 +58,8 @@ object CustomerService { .map { CustomerCreated } .mapError(::exceptionToDomainMessage) - private fun findCustomer(customers: Collection, id: CustomerId): Result { - val customer = customers.find { it.id == id } - return if (customer != null) Ok(customer) else Err(CustomerNotFound) + private fun Collection.findCustomer(id: CustomerId): Result { + return find { it.id == id }.toResultOr { CustomerNotFound } } private fun differenceBetween(old: Customer, new: Customer): EmailAddressChanged? { diff --git a/src/main/kotlin/com/github/michaelbull/result/Result.kt b/src/main/kotlin/com/github/michaelbull/result/Result.kt index f8d3283..6abdf60 100644 --- a/src/main/kotlin/com/github/michaelbull/result/Result.kt +++ b/src/main/kotlin/com/github/michaelbull/result/Result.kt @@ -11,10 +11,10 @@ sealed class Result { 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 of(function: () -> T): Result { + inline fun of(function: () -> V): Result { return try { Ok(function.invoke()) } catch (ex: Exception) { @@ -33,3 +33,14 @@ data class Ok(val value: V) : Result() * Represents a failed [Result], containing an [error]. */ data class Err(val error: E) : Result() + +/** + * 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?.toResultOr(error: () -> E): Result { + return when (this) { + null -> Err(error()) + else -> Ok(this) + } +} diff --git a/src/test/kotlin/com/github/michaelbull/result/ResultTest.kt b/src/test/kotlin/com/github/michaelbull/result/ResultTest.kt index 414393d..a3298a7 100644 --- a/src/test/kotlin/com/github/michaelbull/result/ResultTest.kt +++ b/src/test/kotlin/com/github/michaelbull/result/ResultTest.kt @@ -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() ) } }