diff --git a/ethers-providers/src/main/kotlin/io/ethers/providers/types/Result.kt b/ethers-providers/src/main/kotlin/io/ethers/providers/types/Result.kt new file mode 100644 index 00000000..a51ec594 --- /dev/null +++ b/ethers-providers/src/main/kotlin/io/ethers/providers/types/Result.kt @@ -0,0 +1,117 @@ +package io.ethers.providers.types + +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +// code is optimized to avoid branching operators (if/else) for better JIT compiler optimizations +sealed class Result { + class Success(val value: T) : Result() { + override fun fold(onSuccess: (Success) -> R, onFailure: (Failure) -> R): R { + return onSuccess(this) + } + + override fun toString() = "Success($value)" + } + + class Failure(val error: Error) : Result() { + override fun fold(onSuccess: (Success) -> R, onFailure: (Failure) -> R): R { + return onFailure(this) + } + + override fun toString() = "Failure($error)" + } + + @OptIn(ExperimentalContracts::class) + fun isSuccess(): Boolean { + contract { + returns(true) implies (this@Result is Success) + returns(false) implies (this@Result is Failure) + } + return this is Success + } + + @OptIn(ExperimentalContracts::class) + fun isFailure(): Boolean { + contract { + returns(false) implies (this@Result is Success) + returns(true) implies (this@Result is Failure) + } + return this is Failure + } + + @Suppress("UNCHECKED_CAST") + fun map(mapper: (T) -> R) = fold({ Success(mapper(it.value)) }, { it as Failure }) + fun mapError(mapper: (Error) -> Error) = fold({ it }, { Failure(mapper(it.error)) }) + + inline fun mapTypedError(crossinline mapper: (E) -> Error): Result { + return mapError { it.asTypeOrNull()?.let(mapper) ?: it } + } + + @Suppress("UNCHECKED_CAST") + fun andThen(mapper: (T) -> Result) = fold({ mapper(it.value) }, { it as Failure }) + fun orElse(mapper: (Error) -> Result) = fold({ it }, { mapper(it.error) }) + + 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) }) + + protected abstract fun fold( + onSuccess: (Success) -> R, + onFailure: (Failure) -> R + ): R +} + +fun success(value: T): Result = Result.Success(value) +fun failure(error: Error): Result = Result.Failure(error) + +fun catching(block: () -> R): Result { + return try { + success(block()) + } catch (e: Exception) { + failure(ExceptionError(e)) + } +} + +private data class ExceptionError(val exception: Exception) : Error { + override fun doThrow(): Nothing { + throw RuntimeException("Exceptional execution", exception) + } +} + +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 asTypeOrNull(type: Class): T? { + return if (type.isAssignableFrom(this::class.java)) this as T else null + } +} + +/** + * Cast [Error] to [T] or return null if error is not of type [T]. + * Useful for accessing details of specific error subclass. + */ +inline fun Error.asTypeOrNull(): T? { + return asTypeOrNull(T::class.java) +} + +fun main() { + val response = RpcResponse.result(1) + + val result = success(1) + .map { it.toString() } + .andThen { catching { it.toInt() } } +}