Add toResultOr
Acts as a factory function to convert nullable types to Result types
This commit is contained in:
parent
c93ac7fb18
commit
410563b621
34
README.md
34
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<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
|
||||
|
@ -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) {
|
||||
|
@ -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? {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user