From 29ce45ac232c4c7a9373d6827d91d33c5d642d3d Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Mon, 25 Dec 2023 11:08:53 +0300 Subject: [PATCH 1/5] add new extensions, fix ApiResult() no-arg return value --- .idea/detekt.xml | 7 + .../kotlin/pro/respawn/apiresult/ApiResult.kt | 122 +++++++++++------- .../pro/respawn/apiresult/CollectionResult.kt | 22 ++-- .../pro/respawn/apiresult/SuspendResult.kt | 17 ++- 4 files changed, 102 insertions(+), 66 deletions(-) create mode 100644 .idea/detekt.xml diff --git a/.idea/detekt.xml b/.idea/detekt.xml new file mode 100644 index 0000000..ee7289c --- /dev/null +++ b/.idea/detekt.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/core/src/commonMain/kotlin/pro/respawn/apiresult/ApiResult.kt b/core/src/commonMain/kotlin/pro/respawn/apiresult/ApiResult.kt index 0905d7e..90d54ca 100644 --- a/core/src/commonMain/kotlin/pro/respawn/apiresult/ApiResult.kt +++ b/core/src/commonMain/kotlin/pro/respawn/apiresult/ApiResult.kt @@ -106,6 +106,7 @@ public sealed interface ApiResult { /** * 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. @@ -119,9 +120,10 @@ public sealed interface ApiResult { } /** - * * If T is an exception, will produce [ApiResult.Error] - * * If T is Loading, will produce [ApiResult.Loading] - * * Otherwise [ApiResult.Success] + * * If [T] is an exception, will produce [ApiResult.Error] + * * If [T] is Loading, will produce [ApiResult.Loading] + * * Otherwise [ApiResult.Success]. + * @see asResult */ public inline operator fun invoke(value: T): ApiResult = when (value) { is Loading -> value @@ -130,28 +132,28 @@ public sealed interface ApiResult { } /** - * Returns an ApiResult(Unit) value. + * Returns an [Success] (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 = this + public inline operator fun invoke(): ApiResult = Success(Unit) } } /** * [ApiResult.Error.e]'s stack trace as string */ -public val Error.stackTrace: String get() = e.stackTraceToString() +public inline val Error.stackTrace: String get() = e.stackTraceToString() /** * [ApiResult.Error.e]'s cause */ -public val Error.cause: Throwable? get() = e.cause +public inline val Error.cause: Throwable? get() = e.cause /** * [ApiResult.Error.e]'s message. */ -public val Error.message: String? get() = e.message +public inline val Error.message: String? get() = e.message /** * Execute [block] wrapping it in an [ApiResult] @@ -242,9 +244,8 @@ public inline infix fun ApiResult.onError(block: (Exception) -> Unit): Ap contract { callsInPlace(block, InvocationKind.AT_MOST_ONCE) } - return apply { - if (this is Error) block(e) - } + if (this is Error) block(e) + return this } /** @@ -255,9 +256,8 @@ public inline infix fun ApiResult.onError(block: ( contract { callsInPlace(block, InvocationKind.AT_MOST_ONCE) } - return apply { - if (this is Error && e is E) block(e) - } + if (this is Error && e is E) block(e) + return this } /** @@ -269,7 +269,8 @@ public inline infix fun ApiResult.onSuccess(block: (T) -> Unit): ApiResul contract { callsInPlace(block, InvocationKind.AT_MOST_ONCE) } - return apply { if (this is Success) block(result) } + if (this is Success) block(result) + return this } /** @@ -281,7 +282,8 @@ public inline infix fun ApiResult.onLoading(block: () -> Unit): ApiResult contract { callsInPlace(block, InvocationKind.AT_MOST_ONCE) } - return apply { if (this is Loading) block() } + if (this is Loading) block() + return this } /** @@ -293,19 +295,6 @@ public inline fun ApiResult.errorUnless( predicate: (T) -> Boolean, ): ApiResult = errorIf(exception) { !predicate(it) } -/** - * Makes [this] an [Error] if [predicate] returns false - * @see errorIf - */ -@Deprecated( - "renamed to errorUnless", - ReplaceWith("this.errorUnless(exception, predicate)", "pro.respawn.apiresult.errorUnless") -) -public inline fun ApiResult.errorIfNot( - exception: () -> Exception = { ConditionNotSatisfiedException() }, - predicate: (T) -> Boolean, -): ApiResult = errorUnless(exception, predicate) - /** * Makes [this] an [Error] if [predicate] returns true * @see errorUnless @@ -326,9 +315,16 @@ public inline fun ApiResult.errorIf( */ public inline fun ApiResult.errorOnLoading( exception: () -> Exception = { NotFinishedException() } -): ApiResult = when (this) { - is Loading -> Error(exception()) - else -> this +): ApiResult { + contract { + callsInPlace(exception, InvocationKind.AT_MOST_ONCE) + returns() implies (this@errorOnLoading !is Loading) + } + + return when (this) { + is Loading -> Error(exception()) + else -> this + } } /** @@ -455,13 +451,23 @@ public inline infix fun ApiResult.tryMap(block: (T) -> R): ApiResult ApiResult?.errorOnNull( exception: () -> Exception = { ConditionNotSatisfiedException("Value was null") }, -): ApiResult = this?.errorIf(exception) { it == null }?.map { requireNotNull(it) } ?: Error(exception()) +): ApiResult { + contract { + returns() implies (this@errorOnNull != null) + } + return this?.errorIf(exception) { it == null }?.map { it!! } ?: Error(exception()) +} /** * Maps [Error] values to nulls * @see orNull */ -public inline fun ApiResult.nullOnError(): ApiResult = if (this is Error) Success(null) else this +public inline fun ApiResult.nullOnError(): ApiResult { + contract { + returns() implies (this@nullOnError !is Error) + } + return if (this is Error) Success(null) else this +} /** * Recover from an exception of type [R], else no-op. @@ -482,8 +488,12 @@ public inline infix fun ApiResult.recover( * Recover from an exception. Does not affect [Loading] * See also the typed version of this function to recover from a specific exception type */ -public inline infix fun ApiResult.recover(another: (e: Exception) -> ApiResult): ApiResult = - recover(another) +public inline infix fun ApiResult.recover(another: (e: Exception) -> ApiResult): ApiResult { + contract { + returns() implies (this@recover !is Error) + } + return recover(another) +} /** * calls [recover] catching and wrapping any exceptions thrown inside [block]. @@ -498,23 +508,38 @@ public inline infix fun ApiResult.tryRecover(block */ public inline infix fun ApiResult.tryRecover( block: (e: Exception) -> T -): ApiResult = tryRecover(block) +): ApiResult { + contract { + returns() implies (this@tryRecover !is Error) + } + return tryRecover(block) +} /** * Recover from an [Error] only if the [condition] is true, else no-op. * Does not affect [Loading] - * @see recover + * @see recoverIf + */ +public inline fun ApiResult.tryRecoverIf( + condition: (Exception) -> Boolean, + block: (Exception) -> T, +): ApiResult = recoverIf(condition) { ApiResult { block(it) } } + +/** + * Recover from an [Error] only if the [condition] is true, else no-op. + * Does not affect [Loading] + * @see tryRecoverIf */ public inline fun ApiResult.recoverIf( condition: (Exception) -> Boolean, - block: (Exception) -> T + block: (Exception) -> ApiResult, ): ApiResult { contract { callsInPlace(condition, InvocationKind.AT_MOST_ONCE) callsInPlace(block, InvocationKind.AT_MOST_ONCE) } return when { - this is Error && condition(e) -> Success(block(e)) + this is Error && condition(e) -> block(e) else -> this } } @@ -532,10 +557,7 @@ public inline infix fun ApiResult.chain(another: (T) -> ApiResult<*>): Ap } return when (this) { is Loading, is Error -> this - is Success -> another(result).fold( - onSuccess = { this }, - onError = { Error(it) }, - ) + is Success -> another(result).map { result } } } @@ -575,16 +597,17 @@ public inline infix fun ApiResult.then(another: (T) -> ApiResult): * @see ApiResult.then * @see ApiResult.chain */ -public inline fun ApiResult.flatMap(another: (T) -> ApiResult): ApiResult = then(another) +public inline infix fun ApiResult.flatMap(another: (T) -> ApiResult): ApiResult = then(another) /** - * Makes [this] an error with [IllegalArgumentException] using specified [message] if the [predicate] returns false + * Makes [this] an error with [ConditionNotSatisfiedException] + * using specified [message] if the [predicate] returns false. */ public inline fun ApiResult.require( message: () -> String? = { null }, predicate: (T) -> Boolean ): ApiResult = errorUnless( - exception = { IllegalArgumentException(message()) }, + exception = { ConditionNotSatisfiedException(message()) }, predicate = predicate ) @@ -599,3 +622,8 @@ public inline fun ApiResult<*>.unit(): ApiResult = map {} * @see ApiResult.invoke */ public inline val T.asResult: ApiResult get() = ApiResult(this) + +/** + * Alias for [map] that takes [this] as a parameter + */ +public inline infix fun ApiResult.apply(block: T.() -> R): ApiResult = map(block) diff --git a/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt b/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt index 87a755a..d8baf38 100644 --- a/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt +++ b/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt @@ -18,11 +18,6 @@ import kotlin.jvm.JvmName */ 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 */ @@ -70,7 +65,7 @@ public inline fun > ApiResult.errorIfEmpty( /** * Executes [ApiResult.map] on each value of the collection */ -public inline fun ApiResult>.mapValues( +public inline infix fun ApiResult>.mapValues( transform: (T) -> R ): ApiResult> = map { it.map(transform) } @@ -78,7 +73,7 @@ public inline fun ApiResult>.mapValues( * Executes [ApiResult.map] on each value of the sequence */ @JvmName("sequenceMapValues") -public inline fun ApiResult>.mapValues( +public inline infix fun ApiResult>.mapValues( noinline transform: (T) -> R ): ApiResult> = map { it.map(transform) } @@ -113,8 +108,9 @@ public inline infix fun Sequence>.mapErrors( /** * Filter the underlying collection. */ -public inline infix fun , R> ApiResult.filter(block: (R) -> Boolean): ApiResult> = - map { it.filter(block) } +public inline infix fun , R> ApiResult.filter( + block: (R) -> Boolean +): ApiResult> = map { it.filter(block) } /** * Filter the underlying sequence. @@ -190,20 +186,18 @@ public inline fun Iterable>.values(): List = asSequence() * @see firstSuccessOrThrow */ public inline fun Iterable>.firstSuccess(): ApiResult = - ApiResult { (first { it is Success } as Success).result } + ApiResult { (asSequence().filterIsInstance>().first()).result } /** * Return the first [Success] value, or throw if no success was found * @see firstSuccess * @see firstSuccessOrNull */ -public inline fun Iterable>.firstSuccessOrThrow(): T = - (first { it is Success } as Success).orThrow() +public inline fun Iterable>.firstSuccessOrThrow(): T = firstSuccess().orThrow() /** * Return the first [Success] value, or null if no success was found * @see firstSuccess * @see firstSuccessOrThrow */ -public inline fun Iterable>.firstSuccessOrNull(): T? = - (first { it is Success } as? Success)?.orNull() +public inline fun Iterable>.firstSuccessOrNull(): T? = firstSuccess().orNull() diff --git a/core/src/commonMain/kotlin/pro/respawn/apiresult/SuspendResult.kt b/core/src/commonMain/kotlin/pro/respawn/apiresult/SuspendResult.kt index 6b361dd..dbff883 100644 --- a/core/src/commonMain/kotlin/pro/respawn/apiresult/SuspendResult.kt +++ b/core/src/commonMain/kotlin/pro/respawn/apiresult/SuspendResult.kt @@ -79,7 +79,7 @@ public inline fun Loading.flow( * @see SuspendResult */ public inline fun Flow.asApiResult(): Flow> = this - .map { ApiResult(it) } + .map { it.asResult } .onStart { emit(Loading) } .catchExceptions { emit(Error(it)) } @@ -97,11 +97,18 @@ public inline fun Flow>.mapResults( * * [ApiResult.Companion.invoke] already throws [CancellationException]s. */ -public inline fun ApiResult.rethrowCancellation(): ApiResult = - recover { throw it } +public inline fun ApiResult.rethrowCancellation(): ApiResult = rethrow() /** * 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) } } +public inline fun Flow>.onEachResult( + crossinline block: suspend (T) -> Unit +): Flow> = onEach { result -> result.onSuccess { block(it) } } + +/** + * Invokes [block] each time [this] flow emits an [ApiResult.Success] value + */ +public inline fun Flow>.onEachSuccess( + crossinline block: suspend (T) -> Unit +): Flow> = onEachResult(block) From c12c03ecfc63413b6dd3e05517525f42dc79767a Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Mon, 25 Dec 2023 11:09:12 +0300 Subject: [PATCH 2/5] change NotFinishedException to extend IllegalStateException --- core/src/commonMain/kotlin/pro/respawn/apiresult/Exceptions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/commonMain/kotlin/pro/respawn/apiresult/Exceptions.kt b/core/src/commonMain/kotlin/pro/respawn/apiresult/Exceptions.kt index ed1a1c8..d385ee9 100644 --- a/core/src/commonMain/kotlin/pro/respawn/apiresult/Exceptions.kt +++ b/core/src/commonMain/kotlin/pro/respawn/apiresult/Exceptions.kt @@ -8,7 +8,7 @@ import pro.respawn.apiresult.ApiResult.Loading */ public class NotFinishedException( message: String? = "ApiResult is still in the Loading state", -) : IllegalArgumentException(message) +) : IllegalStateException(message) /** * Exception representing unsatisfied condition when using [errorIf] From fa2bdda3b9387918618ca1c3a89dc7636dd7abe6 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Mon, 25 Dec 2023 11:15:41 +0300 Subject: [PATCH 3/5] update deps --- .../pro/respawn/apiresult/CollectionResult.kt | 2 +- gradle.properties | 10 +++++++--- gradle/libs.versions.toml | 16 ++++++++-------- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt b/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt index d8baf38..d27865e 100644 --- a/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt +++ b/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt @@ -186,7 +186,7 @@ public inline fun Iterable>.values(): List = asSequence() * @see firstSuccessOrThrow */ public inline fun Iterable>.firstSuccess(): ApiResult = - ApiResult { (asSequence().filterIsInstance>().first()).result } + ApiResult { asSequence().filterIsInstance>().first().result } /** * Return the first [Success] value, or throw if no success was found diff --git a/gradle.properties b/gradle.properties index 5843e65..1d8dda8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,8 +1,10 @@ -org.gradle.jvmargs=-Xms3g -Xmx6g -XX:+UseParallelGC -XX:+UseStringDeduplication -Dkotlin.daemon.jvm.options=-Xmx2g -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx6g -XX:+UseParallelGC -XX:+UseStringDeduplication -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=2g +kotlin.daemon.jvmargs=-Xmx6g -XX:+UseParallelGC -XX:+UseStringDeduplication -XX:MaxMetaspaceSize=2g android.useAndroidX=true kotlin.code.style=official org.gradle.caching=true org.gradle.parallel=true +org.gradle.daemon=true android.enableR8.fullMode=true org.gradle.configureondemand=true android.enableJetifier=false @@ -12,9 +14,11 @@ android.experimental.enableSourceSetPathsMap=true android.experimental.cacheCompileLibResources=true kotlin.mpp.enableCInteropCommonization=true kotlin.mpp.stability.nowarn=true +kotlin.mpp.androidGradlePluginCompatibility.nowarn=true org.gradle.unsafe.configuration-cache=true kotlin.mpp.androidSourceSetLayoutVersion=2 android.disableResourceValidation=true android.nonFinalResIds=true -kotlin.mpp.androidGradlePluginCompatibility.nowarn=true -kotlin.native.ignoreIncorrectDependencies=true +kotlinx.atomicfu.enableJvmIrTransformation=true +org.gradle.configuration-cache.problems=warn +nl.littlerobots.vcu.resolver=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index acf23b9..c46a5ac 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,26 +1,26 @@ [versions] compose = "1.5.4" -compose-activity = "1.8.1" -compose-compiler = "1.5.6" -compose-material3 = "1.2.0-alpha12" +compose-activity = "1.8.2" +compose-compiler = "1.5.7" +compose-material3 = "1.2.0-beta01" composeDetektPlugin = "1.3.0" core-ktx = "1.12.0" coroutines = "1.7.3" -dependencyAnalysisPlugin = "1.27.0" +dependencyAnalysisPlugin = "1.28.0" detekt = "1.23.4" detektFormattingPlugin = "1.23.4" dokka = "1.9.10" -gradleAndroid = "8.3.0-alpha17" +gradleAndroid = "8.3.0-alpha18" gradleDoctorPlugin = "0.9.1" kotest = "5.8.0" kotest-plugin = "5.8.0" # @pin -kotlin = "1.9.21" -kotlinx-atomicfu = "0.22.0" +kotlin = "1.9.22" +kotlinx-atomicfu = "0.23.1" lifecycle = "2.6.2" lifecycle-runtime-ktx = "2.6.2" turbine = "1.0.0" -versionCatalogUpdatePlugin = "0.8.1" +versionCatalogUpdatePlugin = "0.8.2" versionsPlugin = "0.50.0" [libraries] From b788dec9dc7dad216337858e9428e047007a2cb6 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Mon, 25 Dec 2023 11:18:00 +0300 Subject: [PATCH 4/5] bump version --- buildSrc/src/main/kotlin/Config.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 757ef6c..a3aa6e1 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -17,7 +17,7 @@ object Config { const val majorRelease = 1 const val minorRelease = 0 - const val patch = 2 + const val patch = 3 const val postfix = "" const val versionName = "$majorRelease.$minorRelease.$patch$postfix" const val url = "https://github.com/respawn-app/ApiResult" From 204643487d81ac4bd35b60f2e1b513fa8ece5a32 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Mon, 25 Dec 2023 11:38:15 +0300 Subject: [PATCH 5/5] fix missing overload of `orEmpty` --- .../kotlin/pro/respawn/apiresult/CollectionResult.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt b/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt index d27865e..78c4a87 100644 --- a/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt +++ b/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt @@ -18,6 +18,11 @@ import kotlin.jvm.JvmName */ public inline fun ApiResult>.orEmpty(): Collection = or(emptyList()) +/** + * Returns [emptyList] if [this]'s list is empty + */ +public inline fun ApiResult>.orEmpty(): List = or(emptyList()) + /** * Returns [emptyList] if [this]'s collection is empty */