-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: make Result error fully typed and add kotlin.Result extensions
- Loading branch information
1 parent
3a9de21
commit d9876fd
Showing
1 changed file
with
226 additions
and
59 deletions.
There are no files selected for viewing
285 changes: 226 additions & 59 deletions
285
ethers-providers/src/main/kotlin/io/ethers/providers/types/Result.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,117 +1,284 @@ | ||
package io.ethers.providers.types | ||
|
||
import kotlin.contracts.ExperimentalContracts | ||
import kotlin.contracts.InvocationKind | ||
import kotlin.contracts.contract | ||
|
||
/** | ||
* Result represents a value that can be either a [Success] or a [Failure]. | ||
* */ | ||
// code is optimized to avoid branching operators (if/else) for better JIT compiler optimizations | ||
sealed class Result<T> { | ||
class Success<T>(val value: T) : Result<T>() { | ||
override fun <R> fold(onSuccess: (Success<T>) -> R, onFailure: (Failure<T>) -> R): R { | ||
sealed class Result<out T : Any?, out E : Result.Error> { | ||
class Success<T : Any?>(val value: T) : Result<T, Nothing>() { | ||
override fun <R> fold( | ||
onSuccess: ResultTransformer<Success<T>, R>, | ||
onFailure: ResultTransformer<Failure<Nothing>, R>, | ||
): R { | ||
return onSuccess(this) | ||
} | ||
|
||
override fun toString() = "Success($value)" | ||
} | ||
|
||
class Failure<T>(val error: Error) : Result<T>() { | ||
override fun <R> fold(onSuccess: (Success<T>) -> R, onFailure: (Failure<T>) -> R): R { | ||
class Failure<E : Error>(val error: E) : Result<Nothing, E>() { | ||
override fun <R> fold( | ||
onSuccess: ResultTransformer<Success<Nothing>, R>, | ||
onFailure: ResultTransformer<Failure<E>, R>, | ||
): R { | ||
return onFailure(this) | ||
} | ||
|
||
override fun toString() = "Failure($error)" | ||
} | ||
|
||
// ideally, "isSuccess"/"isFailure" would be properties, but we cannot define contracts for properties | ||
|
||
/** | ||
* Returns true if [Result] is [Success], false otherwise. | ||
* */ | ||
@OptIn(ExperimentalContracts::class) | ||
fun isSuccess(): Boolean { | ||
contract { | ||
returns(true) implies (this@Result is Success<T>) | ||
returns(false) implies (this@Result is Failure<T>) | ||
returns(true) implies (this@Result is Success<*>) | ||
returns(false) implies (this@Result is Failure<*>) | ||
} | ||
return this is Success<T> | ||
} | ||
|
||
/** | ||
* Returns true if [Result] is [Failure], false otherwise. | ||
* */ | ||
@OptIn(ExperimentalContracts::class) | ||
fun isFailure(): Boolean { | ||
contract { | ||
returns(false) implies (this@Result is Success<T>) | ||
returns(true) implies (this@Result is Failure<T>) | ||
returns(false) implies (this@Result is Success<*>) | ||
returns(true) implies (this@Result is Failure<*>) | ||
} | ||
return this is Failure<T> | ||
return this is Failure<E> | ||
} | ||
|
||
@Suppress("UNCHECKED_CAST") | ||
fun <R> map(mapper: (T) -> R) = fold({ Success(mapper(it.value)) }, { it as Failure<R> }) | ||
fun mapError(mapper: (Error) -> Error) = fold({ it }, { Failure(mapper(it.error)) }) | ||
/** | ||
* Maps a [Result]<[T], [E]> to [Result]<[R], [E]> by applying a function to a [Success] value, leaving a | ||
* [Failure] value untouched. | ||
* */ | ||
fun <R : Any?> map(mapper: ResultTransformer<in T, R>): Result<R, E> = fold({ Success(mapper(it.value)) }, { it }) | ||
|
||
/** | ||
* Maps a [Result]<[T], [E]> to [Result]<[T], [R]> by applying a function to a [Failure], leaving a | ||
* [Success] value untouched. | ||
* */ | ||
fun <R : Error> mapError(mapper: ResultTransformer<in E, out R>): Result<T, R> { | ||
return fold({ it }, { Failure(mapper(it.error)) }) | ||
} | ||
|
||
inline fun <reified E: Error> mapTypedError(crossinline mapper: (E) -> Error): Result<T> { | ||
return mapError { it.asTypeOrNull<E>()?.let(mapper) ?: it } | ||
/** | ||
* Call the function with value of [Success], expecting another result, and skipping if [Result] is [Failure]. | ||
* Useful when chaining multiple fallible operations on the result. | ||
* */ | ||
fun <R : Any?> andThen(mapper: ResultTransformer<in T, Result<R, @UnsafeVariance E>>): Result<R, E> { | ||
return fold({ mapper(it.value) }, { it }) | ||
} | ||
|
||
@Suppress("UNCHECKED_CAST") | ||
fun <R> andThen(mapper: (T) -> Result<R>) = fold({ mapper(it.value) }, { it as Failure<R> }) | ||
fun orElse(mapper: (Error) -> Result<T>) = fold({ it }, { mapper(it.error) }) | ||
/** | ||
* Call the function with error of [Failure], expecting another result, and skipping if [Result] is [Success]. | ||
* Useful when chaining multiple fallible operations on the error (e.g. trying to recover from an error). | ||
* */ | ||
fun <R : Error> orElse(mapper: ResultTransformer<in E, Result<@UnsafeVariance T, R>>): Result<T, R> { | ||
return fold({ it }, { mapper(it.error) }) | ||
} | ||
|
||
/** | ||
* Unwrap the value if [Result] is [Success], or throw an exception if [Result] is [Failure]. | ||
* */ | ||
fun unwrap(): T = fold({ it.value }, { it.error.doThrow() }) | ||
fun unwrapElse(default: T): T = fold({ it.value }, { default }) | ||
fun unwrapOrElse(default: (Error) -> T): T = fold({ it.value }, { default(it.error) }) | ||
|
||
fun onSuccess(block: (T) -> Unit) = fold({ block(it.value) }, {}) | ||
fun onFailure(block: (Error) -> Unit) = fold({}, { block(it.error) }) | ||
/** | ||
* Unwrap the value if [Result] is [Success], or return [default] if [Result] is [Failure]. | ||
* */ | ||
fun unwrapElse(default: @UnsafeVariance T): T = fold({ it.value }, { default }) | ||
|
||
/** | ||
* Unwrap the value if [Result] is [Success], or return the result of [default] function if [Result] is [Failure]. | ||
* */ | ||
fun unwrapOrElse(default: ResultTransformer<in E, @UnsafeVariance T>): T { | ||
return fold({ it.value }, { default(it.error) }) | ||
} | ||
|
||
protected abstract fun <R> fold( | ||
onSuccess: (Success<T>) -> R, | ||
onFailure: (Failure<T>) -> R | ||
): R | ||
} | ||
/** | ||
* Unwrap the error if [Result] is [Failure], or throw an exception if [Result] is [Success]. | ||
* */ | ||
fun unwrapError(): E = fold({ throw IllegalStateException("Cannot unwrap success as error") }, { it.error }) | ||
|
||
fun <T> success(value: T): Result<T> = Result.Success(value) | ||
fun <T> failure(error: Error): Result<T> = Result.Failure(error) | ||
/** | ||
* Unwrap the error if [Result] is [Failure], or return [default] if [Result] is [Success]. | ||
* */ | ||
fun unwrapErrorElse(default: @UnsafeVariance E): E = fold({ default }, { it.error }) | ||
|
||
fun <R> catching(block: () -> R): Result<R> { | ||
return try { | ||
success(block()) | ||
} catch (e: Exception) { | ||
failure(ExceptionError(e)) | ||
/** | ||
* Unwrap the error if [Result] is [Failure], or return the result of [default] function if [Result] is [Success]. | ||
* */ | ||
fun unwrapErrorOrElse(default: ResultTransformer<in T, @UnsafeVariance E>): E { | ||
return fold({ default(it.value) }, { it.error }) | ||
} | ||
} | ||
|
||
private data class ExceptionError(val exception: Exception) : Error { | ||
override fun doThrow(): Nothing { | ||
throw RuntimeException("Exceptional execution", exception) | ||
} | ||
} | ||
/** | ||
* Callback called if [Result] is [Success]. | ||
* */ | ||
fun onSuccess(block: ResultConsumer<in T>) = fold({ block(it.value) }, {}) | ||
|
||
interface Error { | ||
/** | ||
* Throw this [Error] as an exception. If implementation wraps an exception, this method should be overridden | ||
* to provide an accurate stacktrace. | ||
* Callback called if [Result] is [Failure]. | ||
* */ | ||
fun doThrow(): Nothing { | ||
throw RuntimeException(this.toString()) | ||
fun onFailure(block: ResultConsumer<in E>) = fold({}, { block(it.error) }) | ||
|
||
/** | ||
* Call [onSuccess] if [Result] is [Success] or [onFailure] if [Result] is [Failure], returning the result of | ||
* the called function. | ||
* */ | ||
abstract fun <R> fold( | ||
onSuccess: ResultTransformer<Success<@UnsafeVariance T>, R>, | ||
onFailure: ResultTransformer<Failure<@UnsafeVariance E>, R>, | ||
): R | ||
|
||
/** | ||
* Type used for encapsulating error details within [Result.Failure]. | ||
* */ | ||
interface Error { | ||
/** | ||
* Throw this [Error] as an exception. If implementation wraps an exception, this method should be overridden | ||
* to provide an accurate stacktrace. | ||
* */ | ||
fun doThrow(): Nothing { | ||
throw RuntimeException(this.toString()) | ||
} | ||
|
||
/** | ||
* Cast [Error] to a given class or return null if error is not of type [T]. | ||
* Useful for accessing details of specific error subclass. | ||
*/ | ||
@Suppress("UNCHECKED_CAST") | ||
fun <T : Error> asTypeOrNull(type: Class<T>): T? { | ||
return if (type.isAssignableFrom(this::class.java)) this as T else null | ||
} | ||
} | ||
|
||
/** | ||
* Cast [Error] to a given class or return null if error is not of type [T]. | ||
* Cast [Error] to [T] or return null if error is not of type [T]. | ||
* Useful for accessing details of specific error subclass. | ||
*/ | ||
@Suppress("UNCHECKED_CAST") | ||
fun <T : Error> asTypeOrNull(type: Class<T>): T? { | ||
return if (type.isAssignableFrom(this::class.java)) this as T else null | ||
inline fun <reified T : Error> Error.asTypeOrNull(): T? { | ||
return asTypeOrNull(T::class.java) | ||
} | ||
|
||
companion object { | ||
/** | ||
* Return a [Result.Success] with the given [value]. | ||
* */ | ||
@JvmStatic | ||
fun <T : Any?, E : Error> success(value: T): Result<T, E> = Success(value) | ||
|
||
/** | ||
* Return a [Result.Failure] with the given [error]. | ||
* */ | ||
@JvmStatic | ||
fun <T : Any?, E : Error> failure(error: E): Result<T, E> = Failure(error) | ||
} | ||
} | ||
|
||
/** | ||
* Cast [Error] to [T] or return null if error is not of type [T]. | ||
* Useful for accessing details of specific error subclass. | ||
*/ | ||
inline fun <reified T : Error> Error.asTypeOrNull(): T? { | ||
return asTypeOrNull(T::class.java) | ||
* Custom functional interface for better java interop with kotlin lambdas. Prevents the java caller having to | ||
* explicitly return `Unit.INSTANCE`. | ||
* */ | ||
fun interface ResultConsumer<T> { | ||
operator fun invoke(t: T) | ||
} | ||
|
||
/** | ||
* Custom functional interface for better java interop with kotlin lambdas. Prevents the java caller having to | ||
* explicitly return `Unit.INSTANCE`. | ||
* */ | ||
fun interface ResultTransformer<T, R> { | ||
operator fun invoke(t: T): R | ||
} | ||
|
||
fun main() { | ||
val response = RpcResponse.result(1) | ||
/** | ||
* Return a [Result.Success] with the given [value]. | ||
* */ | ||
@JvmSynthetic | ||
fun <T : Any?> success(value: T): Result<T, Nothing> = Result.success(value) | ||
|
||
/** | ||
* Return a [Result.Failure] with the given [error]. | ||
* */ | ||
@JvmSynthetic | ||
fun <E : Result.Error> failure(error: E) = Result.failure<Nothing, E>(error) | ||
|
||
val result = success(1) | ||
.map { it.toString() } | ||
.andThen { catching { it.toInt() } } | ||
/** | ||
* An error that wraps an exception. | ||
* */ | ||
data class ExceptionalError(val cause: Throwable) : Result.Error { | ||
override fun doThrow(): Nothing { | ||
throw RuntimeException("Exceptional execution", cause) | ||
} | ||
} | ||
|
||
// ---------------------------------------------------- // | ||
// ------------- kotlin.Result Extensions ------------- // | ||
// ---------------------------------------------------- // | ||
|
||
@OptIn(ExperimentalContracts::class) | ||
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") | ||
// value returned from mapper will be boxed | ||
inline fun <T, R> kotlin.Result<T>.andThen(mapper: (T) -> kotlin.Result<R>): kotlin.Result<R> { | ||
contract { callsInPlace(mapper, InvocationKind.AT_MOST_ONCE) } | ||
|
||
return when (val v = getOrNull()) { | ||
null -> kotlin.Result(value) | ||
else -> mapper(v) | ||
} | ||
} | ||
|
||
@OptIn(ExperimentalContracts::class) | ||
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") | ||
// value returned from mapper will be boxed | ||
inline fun <T, R> kotlin.Result<T>.andThenCatching(mapper: (T) -> kotlin.Result<R>): kotlin.Result<R> { | ||
contract { callsInPlace(mapper, InvocationKind.AT_MOST_ONCE) } | ||
|
||
return when (val v = getOrNull()) { | ||
null -> kotlin.Result(value) | ||
else -> runCatching { mapper(v).getOrThrow() } | ||
} | ||
} | ||
|
||
/** | ||
* Unwrap the value if [kotlin.Result] is success, or call the [onFailure] function which must either | ||
* throw or return from the enclosing function. This is useful when short-circuiting execution by | ||
* returning from a function on error, e.g.: | ||
* | ||
* ```kotlin | ||
* val input = "invalid input" | ||
* val addr = runCatching { Address(input) }.unwrapOrReturn { cause -> | ||
* return failure(CustomError.InvalidAddress(input, cause)) | ||
* } | ||
* ``` | ||
* */ | ||
@OptIn(ExperimentalContracts::class) | ||
inline fun <R, T : R> kotlin.Result<T>.unwrapOrReturn(onFailure: (ExceptionalError) -> Nothing): R { | ||
contract { callsInPlace(onFailure, InvocationKind.AT_MOST_ONCE) } | ||
|
||
if (isSuccess) { | ||
return this.getOrThrow() | ||
} | ||
onFailure(ExceptionalError(this.exceptionOrNull()!!)) | ||
} | ||
|
||
/** | ||
* Transform [kotlin.Result] into [Result], wrapping the exception in [ExceptionalError] if it holds a failure. | ||
* */ | ||
@Suppress("NOTHING_TO_INLINE") | ||
inline fun <T> kotlin.Result<T>.toResult(): Result<T, ExceptionalError> { | ||
if (isSuccess) { | ||
return success(this.getOrThrow()) | ||
} | ||
return failure(ExceptionalError(this.exceptionOrNull()!!)) | ||
} |