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