diff --git a/README.md b/README.md index c091d2e..c159d43 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,10 @@ enjoy the expanded API of the things you are used to, relying on autocompletion See documentation at [https://opensource.respawn.pro/kmmutils/](https://opensource.respawn.pro/kmmutils/) Javadocs are at [/kmmutils/javadocs](https://opensource.respawn.pro/kmmutils/javadocs/) +## ‼️ ApiResult has moved! Find the new repository and migration guide at https://github.com/respawn-app/ApiResult ‼️ + ### Features -* [ApiResult](https://opensource.respawn.pro/kmmutils/#/apiresult): A monad for wrapping operations that may - fail. Similar to kotlin.Result, but offers extensive, clean DSL and better performance. * [InputForms](https://opensource.respawn.pro/kmmutils/#/inputforms): A stateful and composable text input field validation framework with clean DSL. * [Common](https://opensource.respawn.pro/kmmutils/#/common): Kotlin standard library extensions @@ -36,7 +36,6 @@ Javadocs are at [/kmmutils/javadocs](https://opensource.respawn.pro/kmmutils/jav kmmutils = "< Badge above 👆🏻 >" [dependencies] -kmmutils-apiresult = { module = "pro.respawn.kmmutils:apiresult", version.ref = "kmmutils" } kmmutils-common = { module = "pro.respawn.kmmutils:common", version.ref = "kmmutils" } kmmutils-datetime = { module = "pro.respawn.kmmutils:datetime", version.ref = "kmmutils" } kmmutils-coroutines = { module = "pro.respawn.kmmutils:coroutines", version.ref = "kmmutils" } @@ -44,7 +43,6 @@ kmmutils-inputforms = { module = "pro.respawn.kmmutils:inputforms", version.ref [bundles] kmmutils = [ - "kmmutils-apiresult", "kmmutils-common", "kmmutils-datetime", "kmmutils-coroutines", @@ -55,7 +53,7 @@ kmmutils = [ ### Supported platforms * Android [ `SDK21+` ], -* JVM [ `8+` ], +* JVM [ `11+` ], * iOS [ `x64`, `ArmX64`, `macOSx64`, `macOSArm64` ] * JS [ `browser`, `node.js` ] diff --git a/apiresult/build.gradle.kts b/apiresult/build.gradle.kts deleted file mode 100644 index 1ce6fb2..0000000 --- a/apiresult/build.gradle.kts +++ /dev/null @@ -1,19 +0,0 @@ -plugins { - id("pro.respawn.shared-library") -} - -kotlin { - configureMultiplatform( - this, - android = false, - ios = true, - jvm = true, - js = true, - linux = true, - mingw = true, - ) -} - -dependencies { - commonMainApi(libs.kotlinx.coroutines.core) -} diff --git a/apiresult/src/commonMain/kotlin/pro/respawn/kmmutils/apiresult/ApiResult.kt b/apiresult/src/commonMain/kotlin/pro/respawn/kmmutils/apiresult/ApiResult.kt deleted file mode 100644 index 7232880..0000000 --- a/apiresult/src/commonMain/kotlin/pro/respawn/kmmutils/apiresult/ApiResult.kt +++ /dev/null @@ -1,487 +0,0 @@ -@file:OptIn(ExperimentalContracts::class) -@file:Suppress( - "MemberVisibilityCanBePrivate", - "unused", - "NOTHING_TO_INLINE", - "TooManyFunctions", - "ThrowingExceptionsWithoutMessageOrCause" -) - -package pro.respawn.kmmutils.apiresult - -import pro.respawn.kmmutils.apiresult.ApiResult.Error -import pro.respawn.kmmutils.apiresult.ApiResult.Loading -import pro.respawn.kmmutils.apiresult.ApiResult.Success -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract -import kotlin.coroutines.cancellation.CancellationException -import kotlin.jvm.JvmInline -import kotlin.jvm.JvmName - -/** - * An exception that is thrown when an attempt to retrieve a result of an [ApiResult] is being made when the - * result is [Loading] - */ -public class NotFinishedException( - message: String? = "ApiResult is still in Loading state", -) : IllegalArgumentException(message) - -/** - * Exception representing unsatisfied condition when using [errorIf] - */ -public class ConditionNotSatisfiedException( - message: String? = "ApiResult condition was not satisfied", -) : IllegalArgumentException(message) - -/** - * A class that represents a result of an operation. - * Create an instance with [ApiResult.invoke] and use various operators on the resulting objects. - * This class is **extremely efficient**: no actual objects are created, - * all operations are inlined and no function resolution is performed. - * ApiResult is **not** an Rx-style callback chain - - * the operators that are invoked are called **immediately** and in-place. - */ -public sealed interface ApiResult { - - /** - * A loading state of an [ApiResult] - */ - public data object Loading : ApiResult { - - override fun toString(): String = "ApiResult.Loading" - } - - /** - * A value of [ApiResult] for its successful state. - * @param result a successful result value - */ - @JvmInline - public value class Success(public val result: T) : ApiResult { - - override fun toString(): String = "ApiResult.Success: $result" - } - - /** - * The state of [ApiResult] that represents an error. - * @param e wrapped [Exception] - */ - @JvmInline - public value class Error(public val e: Exception) : ApiResult { - - /** - * [e]'s message. - */ - public val message: String? get() = e.message - - override fun toString(): String = "ApiResult.Error: message=$message and cause: $e" - - /** - * Gets current stack trace as string - */ - public fun asStackTrace(): String = e.stackTraceToString() - } - - /** - * Whether this is [Success] - */ - public val isSuccess: Boolean get() = this is Success - - /** - * Whether this is [Error] - */ - public val isError: Boolean get() = this is Error - - /** - * Whether this is [Loading] - */ - public val isLoading: Boolean get() = this is Loading - - public companion object { - - /** - * Execute [call], catching any exceptions, and wrap it in an [ApiResult]. - * Caught exceptions are mapped to [ApiResult.Error]s. - * [Throwable]s are not caught on purpose. - * [CancellationException]s are rethrown. - */ - public inline operator fun invoke(call: () -> T): ApiResult = try { - Success(call()) - } catch (e: CancellationException) { - throw e - } catch (expected: Exception) { - Error(expected) - } - - /** - * * If T is an exception, will produce [ApiResult.Error] - * * If T is Loading, will produce [ApiResult.Loading] - * * Otherwise [ApiResult.Success] - */ - public inline operator fun invoke(value: T): ApiResult = when (value) { - is Loading -> value - is Exception -> Error(value) - else -> Success(value) - } - - /** - * Returns an ApiResult(Unit) value. - * Use this for applying operators such as `require` and `mapWrapping` to build chains of operators that should - * start with an empty value. - */ - public inline operator fun invoke(): ApiResult = ApiResult(Unit) - } -} - -/** - * Execute [block] wrapping it in an [ApiResult] - * @see ApiResult.invoke - */ -public inline fun T.runResulting(block: T.() -> R): ApiResult = ApiResult { block() } - -/** - * Executes [block], wrapping it in an [ApiResult] - * @see ApiResult.invoke - */ -public inline fun runResulting(block: () -> T): ApiResult = ApiResult { block() } - -/** - * Executes [block] if [this] is an [ApiResult.Error], otherwise returns [ApiResult.Success.result] - * [Loading] will result in [NotFinishedException] - */ -public inline infix fun ApiResult.orElse(block: (e: Exception) -> R): T { - contract { - callsInPlace(block, InvocationKind.AT_MOST_ONCE) - } - return when (this) { - is Success -> result - is Error -> block(e) - is Loading -> block(NotFinishedException()) - } -} - -/** - * If [this] is [Error], returns [defaultValue]. - * @see orElse - */ -public inline infix fun ApiResult.or(defaultValue: R): T = orElse { defaultValue } - -/** - * @return null if [this] is an [ApiResult.Error] or [ApiResult.Loading], otherwise return self. - */ -public inline fun ApiResult.orNull(): T? = or(null) - -/** - * @return exception if [this] is [Error] or null - */ -public inline fun ApiResult.exceptionOrNull(): Exception? = (this as? Error)?.e - -/** - * Throws [ApiResult.Error.e], or [NotFinishedException] if the request has not been completed yet. - */ -public inline fun ApiResult.orThrow(): T = when (this) { - is Loading -> throw NotFinishedException() - is Error -> throw e - is Success -> result -} - -/** - * Fold [this] returning the result of [onSuccess] or [onError] - * By default, maps [Loading] to [Error] with [NotFinishedException] - */ -public inline fun ApiResult.fold( - onSuccess: (result: T) -> R, - onError: (e: Exception) -> R, - noinline onLoading: (() -> R)? = null, -): R { - contract { - callsInPlace(onSuccess, InvocationKind.AT_MOST_ONCE) - callsInPlace(onError, InvocationKind.AT_MOST_ONCE) - } - return when (this) { - is Success -> onSuccess(result) - is Error -> onError(e) - is Loading -> onLoading?.invoke() ?: onError(NotFinishedException()) - } -} - -/** - * Invoke a given [block] if [this] is [Error] - * @see onSuccess - * @see onLoading - */ -public inline fun ApiResult.onError(block: (Exception) -> Unit): ApiResult { - contract { - callsInPlace(block, InvocationKind.AT_MOST_ONCE) - } - return apply { - if (this is Error) block(e) - } -} - -/** - * Invoke a given block if [this] is [Error] and it's [Error.e] is of type [E]. - */ -@JvmName("onErrorTyped") -public inline fun ApiResult.onError(block: (E) -> Unit): ApiResult { - contract { - callsInPlace(block, InvocationKind.AT_MOST_ONCE) - } - return apply { - if (this is Error && e is E) block(e) - } -} - -/** - * Invoke a given [block] if [this] is [Success] - * @see onError - * @see onLoading - */ -public inline fun ApiResult.onSuccess(block: (T) -> Unit): ApiResult { - contract { - callsInPlace(block, InvocationKind.AT_MOST_ONCE) - } - return apply { if (this is Success) block(result) } -} - -/** - * Invoke given [block] if [this] is [Loading] - * @see onError - * @see onSuccess - */ -public inline fun ApiResult.onLoading(block: () -> Unit): ApiResult { - contract { - callsInPlace(block, InvocationKind.AT_MOST_ONCE) - } - return apply { if (this is Loading) block() } -} - -/** - * Makes [this] an [Error] if [predicate] returns false - * @see errorIf - */ -public inline fun ApiResult.errorIfNot( - exception: () -> Exception = { ConditionNotSatisfiedException() }, - predicate: (T) -> Boolean, -): ApiResult = errorIf(exception) { !predicate(it) } - -/** - * Makes [this] an [Error] if [predicate] returns true - * @see errorIfNot - */ -public inline fun ApiResult.errorIf( - exception: () -> Exception = { ConditionNotSatisfiedException() }, - predicate: (T) -> Boolean, -): ApiResult { - contract { - callsInPlace(predicate, InvocationKind.AT_MOST_ONCE) - callsInPlace(exception, InvocationKind.AT_MOST_ONCE) - } - return if (this is Success && predicate(result)) Error(exception()) else this -} - -/** - * Change the type of the [Success] to [R] without affecting [Error]/[Loading] results - * @see mapError - * @see map - * @see mapWrapping - */ -public inline infix fun ApiResult.map(block: (T) -> R): ApiResult { - contract { - callsInPlace(block, InvocationKind.AT_MOST_ONCE) - } - return when (this) { - is Success -> Success(block(result)) - is Error -> Error(e) - is Loading -> this - } -} - -/** - * Maps [Loading] to a [Success], not affecting other states. - * @see mapError - * @see map - * @see mapWrapping - */ -public inline infix fun ApiResult.mapLoading(block: () -> R): ApiResult { - contract { - callsInPlace(block, InvocationKind.AT_MOST_ONCE) - } - return when (this) { - is Success, is Error -> this - is Loading -> Success(block()) - } -} - -/** - * Change the exception of the [Error] response without affecting loading/success results - */ -public inline infix fun ApiResult.mapError(block: (Exception) -> R): ApiResult { - contract { - callsInPlace(block, InvocationKind.AT_MOST_ONCE) - } - return when (this) { - is Success, is Loading -> this - is Error -> Error(block(e)) - } -} - -/** - * Maps the error of the result, if present, to its cause, or self if cause is not available - */ -public inline fun ApiResult.mapErrorToCause(): ApiResult = mapError { it.cause as? Exception ?: it } - -/** - * Unwrap an ApiResult> to be ApiResult - */ -public inline fun ApiResult>.unwrap(): ApiResult = fold( - onSuccess = { it }, - onError = { Error(it) }, - onLoading = { Loading } -) - -/** - * Change the type of successful result to [R], also wrapping [block] - * in another result then folding it (handling exceptions) - * @see map - * @see mapError - * @see mapLoading - */ -public inline infix fun ApiResult.tryMap(block: (T) -> R): ApiResult = - map { ApiResult { block(it) } }.unwrap() - -/** - * Change the type of successful result to [R], also wrapping [block] - * in another result then folding it (handling exceptions) - * @see map - * @see mapError - * @see mapLoading - */ -@Deprecated("use tryMap", ReplaceWith("this.tryMap(block)")) -public inline infix fun ApiResult.mapWrapping(block: (T) -> R): ApiResult = tryMap(block) - -/** - * Make this result an [Error] if [Success] value was null. - * @see errorIfNot - * @see errorIf - * @see errorIfEmpty - */ -public inline fun ApiResult.errorOnNull( - exception: () -> Exception = { ConditionNotSatisfiedException("Value was null") }, -): ApiResult = errorIf(exception) { it == null }.map { requireNotNull(it) } - -/** - * Maps [Error] values to nulls - * @see orNull - */ -public inline fun ApiResult.nullOnError(): ApiResult = if (this is Error) Success(null) else this - -/** - * Recover from an exception of type [R], else no-op. - * Does not affect [Loading]. - * - * Overload for a lambda that already returns an [ApiResult]. - * @see recover - */ -public inline infix fun ApiResult.recover(another: (e: T) -> ApiResult): ApiResult = - when (this) { - is Success, is Loading -> this - is Error -> if (e is T) another(e) else this - } - -@Deprecated("use tryRecover", ReplaceWith("this.tryRecover(block)")) -public inline infix fun ApiResult.recoverWrapping( - block: (T) -> R -): ApiResult = tryRecover(block) - -/** - * calls [recover] catching and wrapping any exceptions thrown inside [block]. - */ -public inline infix fun ApiResult.tryRecover(block: (T) -> R): ApiResult = - when (this) { - is Success, is Loading -> this - is Error -> if (e is T) ApiResult { block(e) } else this - } - -/** - * Recover from an [Error] only if the [condition] is true, else no-op. - * Does not affect [Loading] - * @see recover - */ -public inline fun ApiResult.recoverIf( - condition: (Exception) -> Boolean, - block: (Exception) -> T -): ApiResult { - contract { - callsInPlace(condition, InvocationKind.AT_MOST_ONCE) - callsInPlace(block, InvocationKind.AT_MOST_ONCE) - } - return when (this) { - is Success, is Loading -> this - is Error -> if (condition(e)) Success(block(e)) else this - } -} - -/** - * Call [another] and retrieve the result. - * If the result is success, continue (**the result of calling [another] is discarded**). - * If the result is an error, propagate it to [this]. - * Effectively, requires for another [ApiResult] to succeed before proceeding with this one. - * @see [ApiResult.then] - */ -public inline infix fun ApiResult.chain(another: (T) -> ApiResult<*>): ApiResult { - contract { - callsInPlace(another, InvocationKind.AT_MOST_ONCE) - } - return when (this) { - is Loading, is Error -> this - is Success -> another(result).fold( - onSuccess = { this }, - onError = { Error(it) }, - ) - } -} - -/** - * Call [block], wrapping it in an [ApiResult], and then discard the [Success] value, but propagate [Error]s. - * - * If the result is success, continue (**the result of calling [block] is discarded**). - * If the result is an error, propagate it to [this]. - * - * Alias for [chain] for calls that do not return an ApiResult already. - * @see [ApiResult.chain] - * @see [ApiResult.then] - */ -public inline fun ApiResult.tryChain(block: (T) -> Unit): ApiResult = - chain(another = { ApiResult { block(it) } }) - -/** - * Call [another] and if it succeeds, continue with [another]'s result. - * If it fails, propagate the error. - * Effectively, map to another result. - * @see [ApiResult.chain] - */ -public inline infix fun ApiResult.then(another: (T) -> ApiResult): ApiResult { - contract { - callsInPlace(another, InvocationKind.AT_MOST_ONCE) - } - return map(another).unwrap() -} - -/** - * Makes [this] an error with [IllegalArgumentException] using specified [message] if the [predicate] returns false - */ -public inline fun ApiResult.require( - message: () -> String? = { null }, - predicate: (T) -> Boolean -): ApiResult = - errorIfNot( - exception = { IllegalArgumentException(message()) }, - predicate = predicate, - ) - -/** - * Alias for [errorOnNull] - * @see errorOnNull - */ -public inline fun ApiResult.requireNotNull(): ApiResult = errorOnNull() diff --git a/apiresult/src/commonMain/kotlin/pro/respawn/kmmutils/apiresult/CollectionResult.kt b/apiresult/src/commonMain/kotlin/pro/respawn/kmmutils/apiresult/CollectionResult.kt deleted file mode 100644 index a6a5b58..0000000 --- a/apiresult/src/commonMain/kotlin/pro/respawn/kmmutils/apiresult/CollectionResult.kt +++ /dev/null @@ -1,117 +0,0 @@ -@file:Suppress("MemberVisibilityCanBePrivate", "unused", "TooManyFunctions", "FunctionName", "NOTHING_TO_INLINE") - -package pro.respawn.kmmutils.apiresult - -import pro.respawn.kmmutils.apiresult.ApiResult.Error -import pro.respawn.kmmutils.apiresult.ApiResult.Success -import kotlin.jvm.JvmName - -/** - * Returns [emptyList] if [this]'s collection is empty - */ -public inline fun ApiResult>.orEmpty(): Collection = or(emptyList()) - -/** - * Returns [emptyList] if [this]'s collection is empty - */ -public inline fun ApiResult>.orEmpty(): List = or(emptyList()) - -/** - * Returns [emptyList] if [this]'s collection is empty - */ -public inline fun ApiResult>.orEmpty(): Set = or(emptySet()) - -/** - * Returns [emptyList] if [this]'s collection is empty - */ -public inline fun ApiResult>.orEmpty(): Sequence = or(emptySequence()) - -/** - * Maps every item of [this] using [transform] - */ -public inline fun Iterable>.mapResults( - transform: (T) -> R -): List> = map { it.map(transform) } - -/** - * Maps every item of [this] using [transform] - */ -public inline fun Sequence>.mapResults( - crossinline transform: (T) -> R -): Sequence> = map { it.map(transform) } - -/** - * Maps every [Error] in [this] using [transform] - */ -public inline fun Iterable>.mapErrors( - transform: (Exception) -> Exception -): List> = map { it.mapError(transform) } - -/** - * Maps every [Error] in [this] using [transform] - */ -public inline fun Sequence>.mapErrors( - crossinline transform: (Exception) -> Exception, -): Sequence> = map { it.mapError(transform) } - -/** - * Filters only [Error] values - */ -public inline fun Iterable>.filterErrors(): List = filterIsInstance() - -/** - * Filters only [Error] values - */ -public inline fun Sequence>.filterErrors(): Sequence = filterIsInstance() - -/** - * Filters only [Success] values - */ -public inline fun Iterable>.filterSuccesses(): List> = filterIsInstance>() - -/** - * Filters only [Success] values - */ -public inline fun Sequence>.filterSuccesses(): Sequence> = filterIsInstance>() - -/** - * Filters all null values of [Success]es - */ -public inline fun Iterable>.filterNulls(): List> = - filter { it !is Success || it.result != null }.mapResults { it!! } - -/** - * Filters all null values of [Success]es - */ -public inline fun Sequence>.filterNulls(): Sequence> = - filter { it !is Success || it.result != null }.mapResults { it!! } - -/** - * Maes [this] an [error] if the collection is empty. - */ -public inline fun > ApiResult.errorIfEmpty( - exception: () -> Exception = { ConditionNotSatisfiedException("Collection was empty") }, -): ApiResult = errorIf(exception) { it.none() } - -/** - * Maes [this] an [error] if the collection is empty. - */ -@JvmName("sequenceErrorIfEmpty") -public inline fun > ApiResult.errorIfEmpty( - exception: () -> Exception = { ConditionNotSatisfiedException("Sequence was empty") }, -): ApiResult = errorIf(exception) { it.none() } - -/** - * Executes [ApiResult.map] on each value of the collection - */ -public inline fun ApiResult>.mapValues( - transform: (T) -> R -): ApiResult> = map { it.map(transform) } - -/** - * Executes [ApiResult.map] on each value of the sequence - */ -@JvmName("sequenceMapValues") -public inline fun ApiResult>.mapValues( - noinline transform: (T) -> R -): ApiResult> = map { it.map(transform) } diff --git a/apiresult/src/commonMain/kotlin/pro/respawn/kmmutils/apiresult/SuspendResult.kt b/apiresult/src/commonMain/kotlin/pro/respawn/kmmutils/apiresult/SuspendResult.kt deleted file mode 100644 index 3b83a35..0000000 --- a/apiresult/src/commonMain/kotlin/pro/respawn/kmmutils/apiresult/SuspendResult.kt +++ /dev/null @@ -1,100 +0,0 @@ -@file:Suppress("MemberVisibilityCanBePrivate", "unused", "TooManyFunctions", "FunctionName", "NOTHING_TO_INLINE") - -package pro.respawn.kmmutils.apiresult - -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.supervisorScope -import kotlinx.coroutines.withContext -import pro.respawn.kmmutils.apiresult.ApiResult.Error -import pro.respawn.kmmutils.apiresult.ApiResult.Loading -import pro.respawn.kmmutils.apiresult.ApiResult.Success -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.jvm.JvmName - -/** - * Catches [Exception]s only and rethrows [kotlin.Throwable]s (like [kotlin.Error]s). - */ -public inline fun Flow.catchExceptions( - crossinline action: suspend FlowCollector.(Exception) -> Unit -): Flow = catch { action(it as? Exception ?: throw it) } - -/** - * Run [block] with a given [context], catching any exceptions both in the block and nested coroutines. - * [block] will **not** return until all nested launched coroutines, if any, return. - * For the other type of behavior, use [ApiResult.invoke] directly. - * A failure of a child does not cause the scope to fail and does not affect its other children. - * A failure of the scope itself (exception thrown in the block or external cancellation) - * fails the result with all its children, but does not cancel parent job. - * @see ApiResult.invoke - * @see supervisorScope - * @see kotlinx.coroutines.SupervisorJob - */ -public suspend inline fun SuspendResult( - context: CoroutineContext = EmptyCoroutineContext, - noinline block: suspend CoroutineScope.() -> T, -): ApiResult = withContext(context) { ApiResult { supervisorScope(block) } } - -/** - * Emits [ApiResult.Loading], then executes [call] and [ApiResult]s it. - * @see Flow.asApiResult - */ -public inline fun ApiResult.Companion.flow( - crossinline call: suspend () -> T -): Flow> = kotlinx.coroutines.flow.flow { - emit(Loading) - emit(ApiResult { call() }) -} - -/** - * Emits [ApiResult.Loading], then executes [call] - * @see Flow.asApiResult - */ -@JvmName("flowWithResult") -public inline fun ApiResult.Companion.flow( - crossinline call: suspend () -> ApiResult, -): Flow> = kotlinx.coroutines.flow.flow { - emit(Loading) - emit(call()) -} - -/** - * Emits [Loading] before this flow starts to be collected. - * Then maps all values to [Success] and catches [Exception]s and maps them to [Error]s - * @see ApiResult.Companion.flow - * @see SuspendResult - */ -public inline fun Flow.asApiResult(): Flow> = this - .map { ApiResult(it) } - .onStart { emit(Loading) } - .catchExceptions { emit(Error(it)) } - -/** - * Maps each [Success] value of [this] flow using [transform] - */ -public inline fun Flow>.mapResults( - crossinline transform: suspend (T) -> R -): Flow> = map { result -> result.map { transform(it) } } - -/** - * Throws [CancellationException]s if this is an [Error]. - * - * Important to use this with coroutines if you're not using [SuspendResult] or [ApiResult.Companion.invoke]. - * - * [ApiResult.Companion.invoke] already throws [CancellationException]s. - */ -public inline fun ApiResult.rethrowCancellation(): ApiResult = - recover { throw it } - -/** - * Invokes [block] each time [this] flow emits an [ApiResult.Success] value - */ -public inline fun Flow>.onEachResult(crossinline block: suspend (T) -> Unit): Flow> = - onEach { result -> result.onSuccess { block(it) } } diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 7bce71d..0059b59 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -16,8 +16,8 @@ object Config { const val artifactId = "$group.$artifact" const val majorRelease = 1 - const val minorRelease = 1 - const val patch = 2 + const val minorRelease = 2 + const val patch = 0 const val versionName = "$majorRelease.$minorRelease.$patch" // kotlin diff --git a/docs/_navbar.md b/docs/_navbar.md index c01f2a5..11a2dd5 100644 --- a/docs/_navbar.md +++ b/docs/_navbar.md @@ -1,5 +1,4 @@ * [Home](/) -* [ApiResult](apiresult.md) * [Input Forms](inputforms.md) * [Common](common.md) * [Coroutines](coroutines.md) diff --git a/docs/apiresult.md b/docs/apiresult.md deleted file mode 100644 index c1fbe00..0000000 --- a/docs/apiresult.md +++ /dev/null @@ -1,143 +0,0 @@ -# ApiResult - -Browse -code: [ApiResult](https://github.com/respawn-app/kmmutils/tree/master/apiresult/src/commonMain/kotlin/pro/respawn/kmmutils/apiresult) - -ApiResult is a class that wraps the result of a computation. -Similar to monads, it has 2 main and 1 additional state: - -* Success - contains a value returned by a computation -* Error - wraps an exception caught during a computation -* Loading - intermediate and optional state for async operations - -## Features - -* ApiResult is **extremely lightweight**. It is lighter than kotlin.Result. - All instances of it are `value class`es, all operations are `inline`, which means literally 0 overhead. -* ApiResult offers dozens of operators to cover most of possible use cases to turn your - code from imperative and procedural to declarative and functional, which is way more readable and extensible. -* ApiResult defines a contract that you can use in your code. No one will be able to obtain a result of a computation - without being forced to handle errors at compilation time. - -## Usage - -Example usages cover three main cases: - -* Wrapping a result of a computation -* Wrapping a result of an async computation with multiple coroutines -* Turning a computation into a flow - -```kotlin -// wrap a result of a computation -suspend fun getSubscriptions(userId: String): ApiResult> = ApiResult { - api.getSubscriptions(userId) -} - -// emits: Loading -> User / Error -fun getSubscriptionsAsync(userId: String): Flow>> = ApiResult.flow { - api.getSubscriptions(id) -} - -// SuspendResult will wait for the result of nested coroutines and propagate exceptions thrown in them -suspend fun getVerifiedSubs(userId: String) = SuspendResult { // this: CoroutineScope - val subs = api.getSubscriptions(userId) - - launch { - api.verifySubscriptions(subs) - } - launch { - storage.saveSubsscriptions(subs) - } - - subs -} -``` - -After you create your ApiResult, apply a variety of transformations on it: - -```kotlin -val state: SubscriptionState = repo.getSubscriptions(userId) - .recover { emptyList() } // recover from some or all errors - .mapValues(::SubscriptionModel) // map list items - .map { subs -> subs.filter { it.isPurchased } } // map success value to filtered list - .then { validateSubscriptions(it) } // execute a computation and continue with its result, propagating errors - .onSuccess { updateGracePeriod(it) } // executed on success - .fold( - onSuccess = { SubscriptionState.Subscribed(it) }, - onError = { SubscriptionState.Error(it) }, - ) // unwrap the result to another value -``` - -## Operators - -This is not a comprehensive list of operators as new ones may be added in the future. -Check out source code for a full list. - -### Create: - -* `ApiResult { computation() } ` - wrap the result of a computation -* `ApiResult.flow { computation() }` - produce a flow -* `ApiResult(value) ` - either Error or Success based on the type of the value -* `runResulting { computation() }` - for parity with `runCatching` - -### Handle errors: - -* `or(value)` - returns `value` if the result is an Error -* `orElse { computeValue() }` -* `orNull()` -* `exceptionOrNull()` - if this is an error, returns the exception and discards the result -* `orThrow()` - throw `Error`s -* `fold(onSuccess = { /* ... */ }, onError = { /* ... */ })` - fold the result to type [T] -* `onError { /* ... */ }` -* `onLoading { setLoading(true) }` -* `onSuccess { computation(it) }` - -### Transform: - -* `unwrap()` - sometimes you get into a situation where you have `ApiResult>`. Fix using this operator. -* `chain { anotherCall(it) }` - execute another ApiResult call, - but discard it's Success result and continue with the previous result -* `then { anotherCall(it) }` - execute another ApiResult call and continue with its result type -* `map { it.transform() }` -* `mapWrapping { it.transformThrowing() } ` - map, but catch exceptions in the `transform` block -* `mapLoading { null }` -* `mapError { e -> e.transform() } ` - map only `Error`s -* `mapValues { item -> item.transform() } ` - for collection results -* `errorIf { it.isInvalid }` - error if the predicate is true -* `errorIfNot { it.isAuthorized }` -* `errorOnNull()` -* `require()`, `requireNotNull()` - aliases for `errorIfNot` -* `nullOnError()` - returns `Success` if the result is an error -* `recover { it.handle() }` - recover from a specific exception type -* `recoverIf(condition = { it.isRecoverable }, block = { null })` - -## Notes and usage advice - -* ApiResult is **not** an async scheduling engine like Rx. - As soon as you call an operator on the result, it is executed. -* ApiResult does **not** catch `Throwable`s. This was a purposeful decision. We want to only catch Exceptions that can - be handled. Most `Error`s can not be handled effectively by the application. -* ApiResult does **not** catch `CancellationException`s as they are not meant to be caught. - In case you think you might have wrapped a `CancellationException` in your result, - use `ApiResult.rethrowCancellation()` at the end of the chain. -* Same as `kotlin.Result`, ApiResult is not meant to be passed around to the UI layer. - Be sure not to propagate results everywhere in your code, and handle them on the layer responsible for error handling. - -## How does ApiResult differ from other wrappers? - -* `kotlin.Result` is an existing solution for result wrapping, - however, it's far less performant, less type safe and, most importantly, doesn't offer the declarative api as rich as - ApiResult. You could call ApiResult a successor to `kotlin.Result`. -* ApiResult serves a different purpose than [Sandwich](https://github.com/skydoves/sandwich). - Sandwich specializes in integration with Retrofit and, therefore, is not multiplatform. - ApiResult allows you to wrap any computation, be it Ktor, Retrofit, or database call. ApiResult is more lightweight - and extensible, because it does not hardcode error handling logic. A simple extension on an ApiResult that - uses `mapErrors` will allow you to transform exceptions to your own error types. -* ApiResult is different from [EitherNet](https://github.com/slackhq/EitherNet) because once again - - it doesn't hardcode your error types. ApiResult is multiplatform and lightweight: - no crazy mappings that use reflection to save you from writing 0.5 lines of code to wrap a call in an ApiResult. -* ApiResult is a lighter version of Arrow.kt Monads such as Either. Sometimes you want a monad to wrap your - computations, but don't want to introduce the full complexity and intricacies of Arrow and functional programming. - ApiResult also utilizes existing Exception support in Kotlin instead of trying to wrap any type as an error type. You - can still use sealed interfaces and other features if you subclass Exception from that interface. - ApiResult is easier to understand and use, although less powerful than Arrow. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e529a07..3027eb0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,59 +1,31 @@ [versions] -activity = "1.7.0" -arrow = "1.2.0-RC" -compose = "1.4.0" -compose-compiler = "1.5.1" composeDetektPlugin = "1.3.0" -core-ktx = "1.9.0" coroutines = "1.7.3" datetime = "0.4.0" -dependencyAnalysisPlugin = "1.20.0" +dependencyAnalysisPlugin = "1.21.0" detekt = "1.23.1" detektFormattingPlugin = "1.23.1" dokka = "1.8.20" -flowExt = "0.3.0" -fragment = "1.5.6" -gradleAndroid = "8.1.0" +gradleAndroid = "8.2.0-beta01" gradleDoctorPlugin = "0.8.1" junit = "4.13.2" -koin = "3.4.0" -koin-compose = "3.4.3" kotest = "5.6.2" kotest-plugin = "5.5.5" -# @pin -kotlin = "1.9.0" -kotlinx-atomicfu = "0.20.1" -lifecycle = "2.6.1" -material = "1.8.0" -mockk = "1.13.2" +kotlin = "1.9.10" +kotlinx-atomicfu = "0.22.0" turbine = "1.0.0" versionCatalogUpdatePlugin = "0.8.1" versionsPlugin = "0.47.0" [libraries] android-gradle = { module = "com.android.tools.build:gradle", version.ref = "gradleAndroid" } -androidx-activity = { module = "androidx.activity:activity-ktx", version.ref = "activity" } -androidx-core = { module = "androidx.core:core-ktx", version.ref = "core-ktx" } -androidx-fragment = { module = "androidx.fragment:fragment-ktx", version.ref = "fragment" } -arrow = { module = "io.arrow-kt:arrow-core", version.ref = "arrow" } -compose-activity = { module = "androidx.activity:activity-compose", version.ref = "activity" } -compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" } -compose-material = { module = "androidx.compose.material:material", version.ref = "compose" } -compose-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } -compose-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } -compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } detekt-compose = { module = "ru.kode:detekt-rules-compose", version.ref = "composeDetektPlugin" } detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detektFormattingPlugin" } detekt-gradle = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } detekt-libraries = { module = "io.gitlab.arturbosch.detekt:detekt-rules-libraries", version.ref = "detekt" } dokka-android = { module = "org.jetbrains.dokka:android-documentation-plugin", version.ref = "dokka" } -flowext = { module = "io.github.hoc081098:FlowExt", version.ref = "flowExt" } gradle-versions = { module = "com.github.ben-manes:gradle-versions-plugin", version.ref = "versionsPlugin" } junit = { module = "junit:junit", version.ref = "junit" } -koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } -koin-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin-compose" } -koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } -koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin" } kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" } kotest-framework = { module = "io.kotest:kotest-framework-engine", version.ref = "kotest" } kotest-junit = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" } @@ -65,16 +37,10 @@ kotlinx-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "k kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" } -lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } -lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } version-gradle = { module = "com.github.ben-manes:gradle-versions-plugin", version.ref = "versionsPlugin" } [bundles] -koin = [ - "koin-android", - "koin-core", -] unittest = [ "kotest-assertions", "kotest-framework", diff --git a/settings.gradle.kts b/settings.gradle.kts index 4027cad..e604cbb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -97,7 +97,6 @@ fun RepositoryHandler.ivyNative() { rootProject.name = "kmmutils" -include(":apiresult") include(":common") include(":datetime") include(":coroutines")