From aa2a506f3e4a9db3722d4e0176aba701704e6be7 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sun, 5 Jan 2025 13:13:01 +0100 Subject: [PATCH 01/14] optimize collection operators --- .../pro/respawn/apiresult/CollectionResult.kt | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt b/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt index d19df1b..a244f94 100644 --- a/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt +++ b/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt @@ -53,17 +53,19 @@ public inline infix fun , R> ApiResult.ifEmpty( /** * Makes [this] an [error] if the collection is empty. */ +@Suppress("ThrowingExceptionsWithoutMessageOrCause") // fp public inline fun > ApiResult.errorIfEmpty( exception: () -> Exception = { ConditionNotSatisfiedException("Collection was empty") }, -): ApiResult = errorIf(exception) { it.none() } +): ApiResult = errorIf({ exception() }) { it.none() } /** * Makes [this] an [error] if the collection is empty. */ @JvmName("sequenceErrorIfEmpty") +@Suppress("ThrowingExceptionsWithoutMessageOrCause") // fp public inline fun > ApiResult.errorIfEmpty( exception: () -> Exception = { ConditionNotSatisfiedException("Sequence was empty") }, -): ApiResult = errorIf(exception) { it.none() } +): ApiResult = errorIf({ exception() }) { it.none() } /** * Executes [ApiResult.map] on each value of the collection @@ -147,7 +149,7 @@ public inline fun Sequence>.filterSuccesses(): Sequence = ma * Filters all null values of results */ public inline fun Iterable>.filterNulls(): List> = - filter { !it.isSuccess || it.value != null }.mapResults { it!! } + asSequence().filterNulls().toList() /** * Filters all null values of results @@ -155,6 +157,8 @@ public inline fun Iterable>.filterNulls(): List Sequence>.filterNulls(): Sequence> = filter { !it.isSuccess || it.value != null }.mapResults { it!! } +// TODO: Not possible to provide `vararg` overloads due to https://youtrack.jetbrains.com/issue/KT-33565 + /** * Merges all results into a single [List], or if any has failed, returns [Error]. */ @@ -171,10 +175,7 @@ public inline fun merge(results: Iterable>): ApiResult> */ public inline fun ApiResult.merge( results: Iterable> -): ApiResult> = sequence { - yield(this@merge) - yieldAll(results) -}.asIterable().merge() +): ApiResult> = sequenceOf(this@merge).plus(results).asIterable().merge() /** * Returns a list of only successful values, discarding any errors From 5466226d5c23245d9d520621b6b378a9bf818947 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sun, 5 Jan 2025 13:20:22 +0100 Subject: [PATCH 02/14] provide success value to `errorIf` operators, custom message for `require` --- .../kotlin/pro/respawn/apiresult/ApiResult.kt | 67 ++++++++++++------- .../apiresult/test/ErrorOperatorTests.kt | 5 +- .../apiresult/test/LoadingOperatorTests.kt | 10 ++- .../pro/respawn/apiresult/test/ShouldCall.kt | 5 -- .../apiresult/test/SuccessOperatorTests.kt | 9 +++ 5 files changed, 56 insertions(+), 40 deletions(-) diff --git a/core/src/commonMain/kotlin/pro/respawn/apiresult/ApiResult.kt b/core/src/commonMain/kotlin/pro/respawn/apiresult/ApiResult.kt index 3a466b5..317cdbc 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 value class ApiResult private constructor(@PublishedApi internal v else -> "ApiResult.Success: $value" } + @Suppress("UndocumentedPublicClass", "FunctionName") public companion object { /** @@ -132,6 +133,7 @@ public value class ApiResult private constructor(@PublishedApi internal v */ public inline operator fun invoke(value: T): ApiResult = when (value) { is Exception -> Error(e = value) + is Loading -> Loading() else -> Success(value) } @@ -187,7 +189,9 @@ public inline fun T.runResulting(block: T.() -> R): ApiResult = ApiRes public inline fun runResulting(block: () -> T): ApiResult = ApiResult(call = { block() }) /** - * Executes [block] if [this] is an [ApiResult.Error], otherwise returns [ApiResult.value] + * Executes [block] if [this] is an [ApiResult.Error], otherwise returns [ApiResult.value]. + * + * * [Loading] will result in [NotFinishedException] */ @Suppress("UNCHECKED_CAST") @@ -302,32 +306,32 @@ public inline infix fun ApiResult.onLoading(block: () -> Unit): ApiResult return this } -/** - * Makes [this] an [Error] if [predicate] returns false - * @see errorIf - */ -public inline fun ApiResult.errorUnless( - exception: () -> Exception = { ConditionNotSatisfiedException() }, - predicate: (T) -> Boolean, -): ApiResult = errorIf(exception) { !predicate(it) } - /** * Makes [this] an [Error] if [predicate] returns true * @see errorUnless */ public inline fun ApiResult.errorIf( - exception: () -> Exception = { ConditionNotSatisfiedException() }, + exception: (T) -> Exception = { ConditionNotSatisfiedException() }, predicate: (T) -> Boolean, ): ApiResult { contract { callsInPlace(predicate, InvocationKind.AT_MOST_ONCE) callsInPlace(exception, InvocationKind.AT_MOST_ONCE) } - if (!isSuccess) return this - if (!predicate(value as T)) return this - return Error(e = exception()) + val value = orElse { return this } + if (!predicate(value)) return this + return Error(e = exception(value)) } +/** + * Makes [this] an [Error] if [predicate] returns false + * @see errorIf + */ +public inline fun ApiResult.errorUnless( + exception: (T) -> Exception = { ConditionNotSatisfiedException() }, + predicate: (T) -> Boolean, +): ApiResult = errorIf(exception) { !predicate(it) } + /** * Makes this result an [Error] if [this] result is [Loading] */ @@ -347,7 +351,9 @@ public inline fun ApiResult.errorOnLoading( /** * Alias for [errorOnNull] */ -public inline fun ApiResult?.requireNotNull(): ApiResult = errorOnNull() +public inline fun ApiResult?.requireNotNull( + message: () -> String = { "ApiResult value was null" } +): ApiResult = errorOnNull { IllegalArgumentException(message()) } /** * Alias for [orThrow] @@ -365,14 +371,17 @@ public inline infix fun ApiResult.map(block: (T) -> R): ApiResult { contract { callsInPlace(block, InvocationKind.AT_MOST_ONCE) } - if (isSuccess) return Success(value = block(value as T)) + onSuccess { return ApiResult(block(it)) } return this as ApiResult } /** * Map the [Success] result using [transform], and if the result is not a success, return [default] */ -public inline fun ApiResult.mapOrDefault(default: (e: Exception) -> R, transform: (T) -> R): R { +public inline fun ApiResult.mapOrDefault( + default: (e: Exception) -> R, + transform: (T) -> R +): R { contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) callsInPlace(default, InvocationKind.AT_MOST_ONCE) @@ -405,7 +414,7 @@ public inline infix fun ApiResult.mapLoading(block: () -> R): ApiR * Change the exception of the [Error] response without affecting loading/success results */ public inline infix fun ApiResult.mapError( - block: (Exception) -> Exception + block: (Exception) -> Exception, ): ApiResult = mapError(block) /** @@ -413,7 +422,9 @@ public inline infix fun ApiResult.mapError( * [Loading] and [Success] are unaffected */ @JvmName("mapErrorTyped") -public inline infix fun ApiResult.mapError(block: (R) -> Exception): ApiResult { +public inline infix fun ApiResult.mapError( + block: (R) -> Exception +): ApiResult { contract { callsInPlace(block, InvocationKind.AT_MOST_ONCE) } @@ -424,12 +435,12 @@ public inline infix fun ApiResult.mapError(block: } /** - * Maps the error of the result, if present, to its cause, or self if cause is not available + * 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 + * Unwrap an `ApiResult>` to become `ApiResult` */ public inline fun ApiResult>.unwrap(): ApiResult = when (value) { is Error, is Loading -> this @@ -500,11 +511,13 @@ public inline infix fun ApiResult.recover( * calls [recover] catching and wrapping any exceptions thrown inside [block]. */ @JvmName("tryRecoverTyped") -public inline infix fun ApiResult.tryRecover(block: (T) -> R): ApiResult = - recover(another = { ApiResult(call = { block(it) }) }) +public inline infix fun ApiResult.tryRecover( + block: (T) -> R +): ApiResult = recover(another = { ApiResult(call = { block(it) }) }) /** * Calls [recover] catching and wrapping any exceptions thrown inside [block]. + * * See also the typed version of this function to recover from a specific exception type */ public inline infix fun ApiResult.tryRecover( @@ -545,7 +558,9 @@ public inline fun ApiResult.recoverIf( * 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 { +public inline infix fun ApiResult.chain( + another: (T) -> ApiResult<*> +): ApiResult { contract { callsInPlace(another, InvocationKind.AT_MOST_ONCE) } @@ -596,10 +611,10 @@ public inline infix fun ApiResult.flatMap(another: (T) -> ApiResult * using specified [message] if the [predicate] returns false. */ public inline fun ApiResult.require( - message: () -> String? = { null }, + message: (T) -> String? = { null }, predicate: (T) -> Boolean ): ApiResult = errorUnless( - exception = { ConditionNotSatisfiedException(message()) }, + exception = { ConditionNotSatisfiedException(message(it)) }, predicate = predicate ) diff --git a/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/ErrorOperatorTests.kt b/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/ErrorOperatorTests.kt index fa7a931..3bb4e09 100644 --- a/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/ErrorOperatorTests.kt +++ b/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/ErrorOperatorTests.kt @@ -52,7 +52,6 @@ class ErrorOperatorTests : FreeSpec({ "given success value" - { val result = ApiResult.Error(e = exception) - "then isSuccess should be false" { result.isSuccess shouldBe false } @@ -212,7 +211,7 @@ class ErrorOperatorTests : FreeSpec({ row(ApiResult.Success(value)), row(ApiResult.Loading()), ) { other -> - "for value $other" - { + "for value $other" { result.flatMap { other } shouldBe result } } @@ -221,7 +220,7 @@ class ErrorOperatorTests : FreeSpec({ "then unit does not do anything" { result.unit() shouldBe result } - "then requireIs returns error" - { + "then requireIs returns error" { result.requireIs() shouldBe result } } diff --git a/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/LoadingOperatorTests.kt b/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/LoadingOperatorTests.kt index 4a382ad..877a872 100644 --- a/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/LoadingOperatorTests.kt +++ b/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/LoadingOperatorTests.kt @@ -126,12 +126,10 @@ class LoadingOperatorTests : FreeSpec({ result.requireNotNull() shouldBe result } "then map returns the same value" { - shouldNotCall { - result.map { - it + 1 - markCalled() - } shouldBe result - } + result.map { + fail("Called map") + it + 1 + } shouldBe result } "then mapOrDefault returns new value" { val default = 0 diff --git a/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/ShouldCall.kt b/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/ShouldCall.kt index b445778..38d569b 100644 --- a/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/ShouldCall.kt +++ b/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/ShouldCall.kt @@ -28,8 +28,3 @@ inline infix fun T.shouldCall(crossinline block: CallScope.(value: T) -> Uni this should haveCalled(block) return this } - -inline infix fun T.shouldNotCall(crossinline block: CallScope.(value: T) -> Unit): T { - this shouldNot haveCalled(block) - return this -} diff --git a/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/SuccessOperatorTests.kt b/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/SuccessOperatorTests.kt index 1410658..51168ad 100644 --- a/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/SuccessOperatorTests.kt +++ b/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/SuccessOperatorTests.kt @@ -112,6 +112,15 @@ class SuccessOperatorTests : FreeSpec({ result.errorIf { false }.isError shouldBe false result.errorIf { true }.isError shouldBe true } + "then errorIf provides the success value" { + result.shouldCall { + it.errorIf { + it shouldBe value + markCalled() + false + } + } + } "then errorUnless returns the opposite value" { result.errorUnless { true }.isError shouldBe false result.errorUnless { false }.isError shouldBe true From 24635ec8c77d8ade6fdb4169aa60cfddea53cccc Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sun, 5 Jan 2025 13:20:39 +0100 Subject: [PATCH 03/14] mark `ConditionNotSatisfiedException` as open --- 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 d385ee9..c3e53eb 100644 --- a/core/src/commonMain/kotlin/pro/respawn/apiresult/Exceptions.kt +++ b/core/src/commonMain/kotlin/pro/respawn/apiresult/Exceptions.kt @@ -13,6 +13,6 @@ public class NotFinishedException( /** * Exception representing unsatisfied condition when using [errorIf] */ -public class ConditionNotSatisfiedException( +public open class ConditionNotSatisfiedException( message: String? = "ApiResult condition was not satisfied", ) : IllegalArgumentException(message) From 76d7f6a9cc144fcf13c9bfdc5c9beb4046c07f4e Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sun, 5 Jan 2025 14:16:41 +0100 Subject: [PATCH 04/14] fix error handling, optimize SuspendResult --- .idea/AndroidProjectSystem.xml | 6 ++ .idea/runConfigurations.xml | 17 +++++ .../pro/respawn/apiresult/SuspendResult.kt | 6 +- .../pro/respawn/apiresult/test/ShouldCall.kt | 1 - .../apiresult/test/SuspendResultTests.kt | 75 +++++++++++++++++++ 5 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 .idea/AndroidProjectSystem.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 core/src/jvmTest/kotlin/pro/respawn/apiresult/test/SuspendResultTests.kt diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/core/src/commonMain/kotlin/pro/respawn/apiresult/SuspendResult.kt b/core/src/commonMain/kotlin/pro/respawn/apiresult/SuspendResult.kt index 77a08f5..7ad1ed5 100644 --- a/core/src/commonMain/kotlin/pro/respawn/apiresult/SuspendResult.kt +++ b/core/src/commonMain/kotlin/pro/respawn/apiresult/SuspendResult.kt @@ -11,6 +11,7 @@ package pro.respawn.apiresult import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.catch @@ -46,7 +47,10 @@ public inline fun Flow.catchExceptions( public suspend inline fun SuspendResult( context: CoroutineContext = EmptyCoroutineContext, noinline block: suspend CoroutineScope.() -> T, -): ApiResult = withContext(context) { ApiResult(call = { supervisorScope(block) }) } +): ApiResult { + if (context === EmptyCoroutineContext) return ApiResult(call = { coroutineScope(block) }) + return ApiResult(call = { withContext(context, block) }) +} /** * Emits [ApiResult.Loading], then executes [call] and wraps it. diff --git a/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/ShouldCall.kt b/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/ShouldCall.kt index 38d569b..a93c60e 100644 --- a/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/ShouldCall.kt +++ b/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/ShouldCall.kt @@ -3,7 +3,6 @@ package pro.respawn.apiresult.test import io.kotest.matchers.Matcher import io.kotest.matchers.MatcherResult import io.kotest.matchers.should -import io.kotest.matchers.shouldNot inline fun haveCalled(crossinline block: CallScope.(value: T) -> Unit) = Matcher { val scope = CallScope() diff --git a/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/SuspendResultTests.kt b/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/SuspendResultTests.kt new file mode 100644 index 0000000..f594ece --- /dev/null +++ b/core/src/jvmTest/kotlin/pro/respawn/apiresult/test/SuspendResultTests.kt @@ -0,0 +1,75 @@ +package pro.respawn.apiresult.test + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.core.test.testCoroutineScheduler +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import pro.respawn.apiresult.SuspendResult +import pro.respawn.apiresult.exceptionOrNull +import kotlin.coroutines.EmptyCoroutineContext + +@OptIn(ExperimentalStdlibApi::class) +class SuspendResultTests : FreeSpec({ + coroutineTestScope = true + + val e = IllegalStateException("Failure") + + "Given empty context" - { + val ctx = EmptyCoroutineContext + "And SuspendResult that throws in a coroutine" - { + val result = SuspendResult(ctx) { + launch { throw e } + } + "Then exception is wrapped" { + result.exceptionOrNull() shouldBe e + } + } + "And SuspendResult that throws in asyncs" - { + val result = SuspendResult(ctx) { + async { 42 } + async { throw e } + } + "Then exception is wrapped" { + result.exceptionOrNull() shouldBe e + } + } + "And SuspendResult that nests coroutines" - { + val result = SuspendResult(ctx) { + launch { launch { throw e } } + } + "Then exception is wrapped" { + result.exceptionOrNull() shouldBe e + } + } + "And SuspendResult that throws in another context" - { + val result = SuspendResult(ctx) { + launch(Dispatchers.Default) { throw e } + } + "Then exception is wrapped" { + result.exceptionOrNull() shouldBe e + } + } + } + "Given non-empty context" - { + val ctx = UnconfinedTestDispatcher(testCoroutineScheduler, "Ctx") + "And SuspendResult that throws in a coroutine" - { + val result = SuspendResult(ctx) { + launch { throw e } + } + "Then exception is wrapped" { + result.exceptionOrNull() shouldBe e + } + } + "And SuspendResult that nests coroutines" - { + val result = SuspendResult(ctx) { + launch { launch { throw e } } + } + "Then exception is wrapped" { + result.exceptionOrNull() shouldBe e + } + } + } +}) From cfa4040569a1241f19be81f9f89d25feb70fe7a7 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sun, 5 Jan 2025 14:24:53 +0100 Subject: [PATCH 05/14] bump gradle --- gradle/wrapper/gradle-wrapper.jar | Bin 43504 -> 43583 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 3 +-- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 2c3521197d7c4586c843d1d3e9090525f1898cde..a4b76b9530d66f5e68d973ea569d8e19de379189 100644 GIT binary patch delta 3990 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 09523c0..cea7a79 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f5feea6..f3b75f3 100755 --- a/gradlew +++ b/gradlew @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum From ce7adb91643bc1864eb9befe117a3df017bbb8ba Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sun, 5 Jan 2025 14:28:13 +0100 Subject: [PATCH 06/14] update gradle props --- .github/ci-gradle.properties | 33 +++++++++++++++++++++++++-------- gradle.properties | 32 ++++++++++++++++++++++++++------ 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/.github/ci-gradle.properties b/.github/ci-gradle.properties index 6489d6c..b517966 100644 --- a/.github/ci-gradle.properties +++ b/.github/ci-gradle.properties @@ -1,5 +1,6 @@ -org.gradle.jvmargs=-Xmx3g -Xms1g -XX:+UseParallelGC -XX:+UseStringDeduplication -Dfile.encoding=UTF-8 -kotlin.daemon.jvmargs=-Xmx3g -Xms1g -XX:+UseParallelGC -XX:+UseStringDeduplication -XX:MaxMetaspaceSize=1g +# suppress inspection "UnusedProperty" for whole file +org.gradle.jvmargs=-Xmx6g -Xms1g -XX:+UseParallelGC -XX:+UseStringDeduplication -Dfile.encoding=UTF-8 +kotlin.daemon.jvmargs=-Xmx6g -Xms1g -XX:+UseParallelGC -XX:+UseStringDeduplication -XX:MaxMetaspaceSize=2g android.useAndroidX=true kotlin.code.style=official org.gradle.caching=true @@ -8,21 +9,37 @@ android.enableR8.fullMode=true org.gradle.configureondemand=true android.enableJetifier=false kotlin.incremental.usePreciseJavaTracking=true +org.gradle.configuration-cache.problems=warn android.nonTransitiveRClass=true 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=false -org.gradle.daemon=true +org.gradle.daemon=false android.nonFinalResIds=true -kotlin.native.ignoreIncorrectDependencies=true kotlinx.atomicfu.enableJvmIrTransformation=true -org.jetbrains.compose.experimental.macos.enabled=true -org.gradle.configuration-cache.problems=warn +android.lint.useK2Uast=true nl.littlerobots.vcu.resolver=true -org.gradle.console=plain +org.jetbrains.compose.experimental.jscanvas.enabled=true +org.jetbrains.compose.experimental.wasm.enabled=true +org.jetbrains.compose.experimental.macos.enabled=true +# Do not garbage collect on timeout on native when appExtensions are used and app is in bacground +kotlin.native.binary.appStateTracking=enabled +# Lift main thread suspending function invocation restriction +kotlin.native.binary.objcExportSuspendFunctionLaunchThreadRestriction=none +# Native incremental compilation +kotlin.incremental.native=true +android.experimental.additionalArtifactsInModel=true +kotlin.apple.xcodeCompatibility.nowarn=true +# Enable new k/n GC +kotlin.native.binary.gc=cms +org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled +org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true +org.gradle.configuration-cache.parallel=true release=true +#kotlin.kmp.isolated-projects.support=enable +kotlin.incremental.wasm=true +#org.gradle.unsafe.isolated-projects=true diff --git a/gradle.properties b/gradle.properties index 09a2657..c2f555b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,25 +1,45 @@ -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 +# suppress inspection "UnusedProperty" for whole file +org.gradle.jvmargs=-Xmx6g -Xms1g -XX:+UseParallelGC -XX:+UseStringDeduplication -Dfile.encoding=UTF-8 +kotlin.daemon.jvmargs=-Xmx6g -Xms1g -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 kotlin.incremental.usePreciseJavaTracking=true +org.gradle.configuration-cache.problems=warn android.nonTransitiveRClass=true 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 +org.gradle.daemon=true android.nonFinalResIds=true kotlinx.atomicfu.enableJvmIrTransformation=true -org.gradle.configuration-cache.problems=warn +android.lint.useK2Uast=true nl.littlerobots.vcu.resolver=true -release=false +org.jetbrains.compose.experimental.jscanvas.enabled=true +org.jetbrains.compose.experimental.wasm.enabled=true +org.jetbrains.compose.experimental.macos.enabled=true +# Do not garbage collect on timeout on native when appExtensions are used and app is in bacground +kotlin.native.binary.appStateTracking=enabled +# Lift main thread suspending function invocation restriction +kotlin.native.binary.objcExportSuspendFunctionLaunchThreadRestriction=none +# Native incremental compilation +kotlin.incremental.native=true +android.experimental.additionalArtifactsInModel=true +kotlin.apple.xcodeCompatibility.nowarn=true +# Enable new k/n GC +kotlin.native.binary.gc=cms +org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled +org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true +org.gradle.configuration-cache.parallel=true +release=true +#kotlin.kmp.isolated-projects.support=enable +kotlin.incremental.wasm=true +#org.gradle.unsafe.isolated-projects=true From 0353ae47d2a708927ccdc246c8518a7acf131ead Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sun, 5 Jan 2025 15:14:13 +0100 Subject: [PATCH 07/14] update deps [wip] --- gradle/libs.versions.toml | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 10180ad..31f6817 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,22 +1,21 @@ [versions] -compose = "1.7.0-beta07" -compose-activity = "1.9.1" -compose-material3 = "1.3.0-beta05" -composeDetektPlugin = "1.3.0" -core-ktx = "1.13.1" -coroutines = "1.9.0-RC" -dependencyAnalysisPlugin = "1.32.0" -detekt = "1.23.6" -detektFormattingPlugin = "1.23.6" -dokka = "1.9.20" -gradleAndroid = "8.6.0-rc01" +atomicfu = "0.26.0" +compose = "1.7.6" +compose-activity = "1.10.0-rc01" +compose-material3 = "1.3.1" +composeDetektPlugin = "1.4.0" +core-ktx = "1.15.0" +coroutines = "1.10.1" +dependencyAnalysisPlugin = "2.6.1" +detekt = "1.23.7" +detektFormattingPlugin = "1.23.7" +dokka = "2.0.0" +gradleAndroid = "8.8.0-rc02" gradleDoctorPlugin = "0.10.0" -kotest = "5.9.1" -# @pin -kotlin = "2.0.10" -atomicfu = "0.25.0" -lifecycle = "2.8.4" -maven-publish-plugin = "0.29.0" +kotest = "6.0.0.M1" +kotlin = "2.1.0" +lifecycle = "2.8.7" +maven-publish-plugin = "0.30.0" turbine = "1.0.0" versionCatalogUpdatePlugin = "0.8.4" @@ -47,6 +46,7 @@ kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version. kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test-common", version.ref = "kotlin" } lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } +dokka-gradle = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } [bundles] unittest = [ From 4387fd0fdc948d7208153b116731945c15abf078 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sun, 5 Jan 2025 15:14:30 +0100 Subject: [PATCH 08/14] update build configuration to match new deps --- .github/workflows/ci.yml | 2 +- .github/workflows/docs.yml | 7 +- build.gradle.kts | 64 ++++++++++--------- buildSrc/build.gradle.kts | 1 + .../main/kotlin/dokkaDocumentation.gradle.kts | 37 +++++++++++ core/build.gradle.kts | 2 + 6 files changed, 80 insertions(+), 33 deletions(-) create mode 100644 buildSrc/src/main/kotlin/dokkaDocumentation.gradle.kts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf42574..fc20b40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: with: distribution: 'zulu' check-latest: true - java-version: 22 + java-version: 23 cache: 'gradle' - name: Validate gradle wrapper diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 230acc8..3c450f4 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -39,10 +39,13 @@ jobs: run: cp ./README.md ./docs/README.md - name: Generate docs - run: ./gradlew :dokkaHtmlMultiModule --no-configuration-cache + run: ./gradlew dokkaGenerate + + - name: Make javadoc dir + run: mkdir -p ./docs/javadocs - name: Move docs to the parent docs dir - run: cp -r ./build/dokka/htmlMultiModule/ ./docs/javadocs/ + run: cp -r ./build/dokka/html/ ./docs/javadocs - name: Setup Pages uses: actions/configure-pages@v5 diff --git a/build.gradle.kts b/build.gradle.kts index 3e4b967..eea104b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,23 +1,28 @@ +import com.vanniktech.maven.publish.JavadocJar +import com.vanniktech.maven.publish.KotlinMultiplatform import com.vanniktech.maven.publish.MavenPublishBaseExtension +import com.vanniktech.maven.publish.MavenPublishBasePlugin import com.vanniktech.maven.publish.SonatypeHost import nl.littlerobots.vcu.plugin.versionCatalogUpdate import nl.littlerobots.vcu.plugin.versionSelector +import org.gradle.kotlin.dsl.withType import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradleSubplugin +import org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockMismatchReport import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { alias(libs.plugins.detekt) alias(libs.plugins.gradleDoctor) alias(libs.plugins.version.catalog.update) - alias(libs.plugins.dokka) - alias(libs.plugins.atomicfu) + // alias(libs.plugins.atomicfu) alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.maven.publish) apply false + dokkaDocumentation // plugins already on a classpath (conventions) + // alias(libs.plugins.dokka) apply false // alias(libs.plugins.androidApplication) apply false // alias(libs.plugins.androidLibrary) apply false // alias(libs.plugins.kotlinMultiplatform) apply false @@ -31,10 +36,8 @@ allprojects { subprojects { plugins.withType().configureEach { the().apply { - enableIntrinsicRemember = true - enableNonSkippingGroupOptimization = true - enableStrongSkippingMode = true - stabilityConfigurationFile = rootProject.layout.projectDirectory.file("stability_definitions.txt") + featureFlags.addAll(ComposeFeatureFlag.OptimizeNonSkippingGroups) + stabilityConfigurationFiles.add(rootProject.layout.projectDirectory.file("stability_definitions.txt")) if (properties["enableComposeCompilerReports"] == "true") { val metricsDir = layout.buildDirectory.dir("compose_metrics") metricsDestination = metricsDir @@ -42,9 +45,16 @@ subprojects { } } } - afterEvaluate { - extensions.findByType()?.run { + plugins.withType { + the().apply { val isReleaseBuild = properties["release"]?.toString().toBoolean() + configure( + KotlinMultiplatform( + javadocJar = JavadocJar.Empty(), + sourcesJar = true, + androidVariantsToPublish = listOf("release"), + ) + ) publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, false) if (isReleaseBuild) signAllPublications() coordinates(Config.artifactId, name, Config.version(isReleaseBuild)) @@ -79,21 +89,6 @@ subprojects { useJUnitPlatform() filter { isFailOnNoMatchingTests = true } } - withType().configureEach { - compilerOptions { - jvmTarget.set(Config.jvmTarget) - freeCompilerArgs.apply { addAll(Config.jvmCompilerArgs) } - optIn.addAll(Config.optIns.map { "-opt-in=$it" }) - } - } - } - - if (name == "app") return@subprojects - - apply(plugin = rootProject.libs.plugins.dokka.id) - - dependencies { - dokkaPlugin(rootProject.libs.dokka.android) } } @@ -121,12 +116,10 @@ versionCatalogUpdate { } } -atomicfu { - dependenciesVersion = libs.versions.atomicfu.get() - transformJvm = false - jvmVariant = "VH" - transformJs = false -} +// atomicfu { +// dependenciesVersion = libs.versions.atomicfu.get() +// jvmVariant = "VH" +// } tasks { withType().configureEach { @@ -165,3 +158,14 @@ rootProject.plugins.withType().configureEach { yarnLockAutoReplace = true } } + +dependencies { + detektPlugins(rootProject.libs.detekt.formatting) + detektPlugins(rootProject.libs.detekt.compose) + detektPlugins(rootProject.libs.detekt.libraries) + projects.run { + listOf( + core, + ).forEach { dokka(it) } + } +} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index a88ee7a..4909b97 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -6,4 +6,5 @@ plugins { dependencies { implementation(libs.android.gradle) implementation(libs.kotlin.gradle) + implementation(libs.dokka.gradle) } diff --git a/buildSrc/src/main/kotlin/dokkaDocumentation.gradle.kts b/buildSrc/src/main/kotlin/dokkaDocumentation.gradle.kts new file mode 100644 index 0000000..35cc93b --- /dev/null +++ b/buildSrc/src/main/kotlin/dokkaDocumentation.gradle.kts @@ -0,0 +1,37 @@ +import org.jetbrains.dokka.gradle.engine.parameters.VisibilityModifier + +plugins { + id("org.jetbrains.dokka") + // id("org.jetbrains.dokka-javadoc") +} + +val libs by versionCatalog + +dokka { + dokkaGeneratorIsolation = ClassLoaderIsolation() + moduleName = project.name + moduleVersion = project.version.toString() + pluginsConfiguration.html { + footerMessage = "© ${Config.vendorName}" + homepageLink = Config.url + } + dokkaPublications.configureEach { + suppressInheritedMembers = false + suppressObviousFunctions = true + } + dokkaSourceSets.configureEach { + reportUndocumented = false + enableJdkDocumentationLink = true + enableAndroidDocumentationLink = true + enableKotlinStdLibDocumentationLink = true + skipEmptyPackages = true + skipDeprecated = true + jdkVersion = Config.javaVersion.majorVersion.toInt() + documentedVisibilities(VisibilityModifier.Public) + } + // remoteUrl = Config.docsUrl +} + +dependencies { + dokkaPlugin(libs.requireLib("dokka-android")) +} diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 951e098..88d3d93 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("pro.respawn.shared-library") alias(libs.plugins.maven.publish) + dokkaDocumentation // alias(libs.plugins.atomicfu) } @@ -10,5 +11,6 @@ android { dependencies { commonMainApi(libs.kotlin.coroutines.core) + jvmTestImplementation(libs.bundles.unittest) } From f28438b0d0035acc1d94a045c9ecf3fa3a105fb0 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sun, 5 Jan 2025 15:18:18 +0100 Subject: [PATCH 09/14] update ci jvm --- .github/workflows/docs.yml | 2 +- .github/workflows/publish.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 3c450f4..a2e02d4 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -29,7 +29,7 @@ jobs: with: distribution: 'zulu' check-latest: true - java-version: 22 + java-version: 23 cache: 'gradle' - name: Validate gradle wrapper diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 518a010..9df5f8b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -31,7 +31,7 @@ jobs: with: distribution: 'zulu' check-latest: true - java-version: 22 + java-version: 23 cache: 'gradle' - name: Validate gradle wrapper From 3c143aaf56792032d58febe34cd41655963f30d7 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sun, 5 Jan 2025 15:26:27 +0100 Subject: [PATCH 10/14] bump minor version, target sdk 35 --- buildSrc/src/main/kotlin/Config.kt | 35 ++++++++++++++++++------------ 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 982c20e..c91eb6a 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -16,7 +16,7 @@ object Config { const val artifactId = "$group.$artifact" const val majorRelease = 2 - const val minorRelease = 0 + const val minorRelease = 1 const val patch = 0 const val postfix = "" // include dash const val versionName = "$majorRelease.$minorRelease.$patch$postfix" @@ -25,10 +25,8 @@ object Config { const val licenseUrl = "http://www.apache.org/licenses/LICENSE-2.0.txt" const val scmUrl = "https://github.com/respawn-app/ApiResult.git" const val name = "ApiResult" - const val description = """ -ApiResult is a Kotlin Multiplatform declarative error handling framework that is performant, easy to use and -feature-rich. - """ + @Suppress("MaxLineLength") + const val description = """ApiResult is a Kotlin Multiplatform declarative error handling library. Just like Arrow's Either, but without the complexity.""" const val supportEmail = "hello@respawn.pro" const val vendorName = "Respawn Open Source Team" const val vendorId = "respawn-app" @@ -36,31 +34,40 @@ feature-rich. // kotlin + val jvmTarget = JvmTarget.JVM_11 + val javaVersion = JavaVersion.VERSION_11 + const val compileSdk = 35 + const val targetSdk = compileSdk + const val minSdk = 21 + const val appMinSdk = 26 + const val publishingVariant = "release" + val optIns = listOf( "kotlinx.coroutines.ExperimentalCoroutinesApi", "kotlinx.coroutines.FlowPreview", "kotlin.RequiresOptIn", "kotlin.experimental.ExperimentalTypeInference", - "kotlin.contracts.ExperimentalContracts" + "kotlin.uuid.ExperimentalUuidApi", + "kotlin.contracts.ExperimentalContracts", ) val compilerArgs = listOf( "-Xbackend-threads=0", // parallel IR compilation + "-Xexpect-actual-classes", + "-Xwasm-use-new-exception-proposal", + "-Xconsistent-data-class-copy-visibility", + "-Xsuppress-warning=NOTHING_TO_INLINE", + "-Xsuppress-warning=UNUSED_ANONYMOUS_PARAMETER", + "-Xwasm-debugger-custom-formatters" ) val jvmCompilerArgs = buildList { addAll(compilerArgs) add("-Xjvm-default=all") // enable all jvm optimizations add("-Xcontext-receivers") add("-Xstring-concat=inline") - addAll(optIns.map { "-opt-in=$it" }) + add("-Xlambdas=indy") + add("-Xjdk-release=${jvmTarget.target}") } - val jvmTarget = JvmTarget.JVM_11 - val javaVersion = JavaVersion.VERSION_11 - const val compileSdk = 34 - const val targetSdk = compileSdk - const val minSdk = 21 - const val appMinSdk = 26 - const val publishingVariant = "release" // android const val namespace = artifactId From dd88c7717dde329dd224aed5ca129cabbe534f4c Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sun, 5 Jan 2025 15:26:42 +0100 Subject: [PATCH 11/14] enable wasmWasi target, update kmp build scripts --- .../src/main/kotlin/ConfigureMultiplatform.kt | 28 +++++++++++++++---- buildSrc/src/main/kotlin/Util.kt | 17 ++++++++++- .../main/kotlin/dokkaDocumentation.gradle.kts | 1 + .../pro.respawn.android-library.gradle.kts | 16 +++++++++++ .../pro.respawn.shared-library.gradle.kts | 2 -- 5 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 buildSrc/src/main/kotlin/pro.respawn.android-library.gradle.kts diff --git a/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt b/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt index 19ae47e..51de264 100644 --- a/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt +++ b/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt @@ -1,14 +1,14 @@ -@file:Suppress("MissingPackageDeclaration", "unused", "UndocumentedPublicFunction", "LongMethod") +@file:Suppress("MissingPackageDeclaration", "unused", "UndocumentedPublicFunction", "LongMethod", "UnusedImports") import org.gradle.api.Project import org.gradle.kotlin.dsl.getValue import org.gradle.kotlin.dsl.getting import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.plugin.KotlinHierarchyBuilder -import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl -@OptIn(ExperimentalWasmDsl::class, ExperimentalKotlinGradlePluginApi::class) +@OptIn(ExperimentalKotlinGradlePluginApi::class, ExperimentalWasmDsl::class) fun Project.configureMultiplatform( ext: KotlinMultiplatformExtension, jvm: Boolean = true, @@ -22,12 +22,19 @@ fun Project.configureMultiplatform( windows: Boolean = true, wasmJs: Boolean = true, wasmWasi: Boolean = true, + explicitApi: Boolean = true, configure: KotlinHierarchyBuilder.Root.() -> Unit = {}, ) = ext.apply { val libs by versionCatalog - explicitApi() + if (explicitApi) explicitApi() applyDefaultHierarchyTemplate(configure) withSourcesJar(true) + compilerOptions { + extraWarnings.set(true) + freeCompilerArgs.addAll(Config.compilerArgs) + optIn.addAll(Config.optIns) + progressiveMode.set(true) + } if (linux) { linuxX64() @@ -54,10 +61,19 @@ fun Project.configureMultiplatform( } if (android) androidTarget { - publishLibraryVariants("release") + publishLibraryVariants(Config.publishingVariant) + compilerOptions { + jvmTarget.set(Config.jvmTarget) + freeCompilerArgs.addAll(Config.jvmCompilerArgs) + } } - if (jvm) jvm() + if (jvm) jvm { + compilerOptions { + jvmTarget.set(Config.jvmTarget) + freeCompilerArgs.addAll(Config.jvmCompilerArgs) + } + } sequence { if (iOs) { diff --git a/buildSrc/src/main/kotlin/Util.kt b/buildSrc/src/main/kotlin/Util.kt index 9e93d23..08154cc 100644 --- a/buildSrc/src/main/kotlin/Util.kt +++ b/buildSrc/src/main/kotlin/Util.kt @@ -5,7 +5,10 @@ import org.gradle.api.artifacts.VersionCatalog import org.gradle.api.artifacts.VersionCatalogsExtension import org.gradle.kotlin.dsl.getByType import org.gradle.plugin.use.PluginDependency +import java.io.File +import java.io.FileInputStream import java.util.Base64 +import java.util.Properties /** * Load version catalog for usage in places where it is not available yet with gradle 7.x. @@ -46,6 +49,17 @@ fun List.toJavaArrayString() = buildString { fun String.toBase64() = Base64.getEncoder().encodeToString(toByteArray()) +fun Project.localProperties() = lazy { + Properties().apply { + val file = File(rootProject.rootDir.absolutePath, "local.properties") + if (!file.exists()) { + println("w: Local.properties file does not exist. You may be missing some publishing keys") + return@apply + } + load(FileInputStream(file)) + } +} + fun stabilityLevel(version: String): Int { Config.stabilityLevels.forEachIndexed { index, postfix -> val regex = """.*[.\-]$postfix[.\-\d]*""".toRegex(RegexOption.IGNORE_CASE) @@ -53,8 +67,9 @@ fun stabilityLevel(version: String): Int { } return Config.stabilityLevels.size } - fun Config.version(isRelease: Boolean) = buildString { append(versionName) if (!isRelease) append("-SNAPSHOT") } + +fun Project.namespaceByPath() = "${Config.namespace}.${path.replace(":", ".").removePrefix(".")}" diff --git a/buildSrc/src/main/kotlin/dokkaDocumentation.gradle.kts b/buildSrc/src/main/kotlin/dokkaDocumentation.gradle.kts index 35cc93b..e2905fa 100644 --- a/buildSrc/src/main/kotlin/dokkaDocumentation.gradle.kts +++ b/buildSrc/src/main/kotlin/dokkaDocumentation.gradle.kts @@ -13,6 +13,7 @@ dokka { moduleVersion = project.version.toString() pluginsConfiguration.html { footerMessage = "© ${Config.vendorName}" + customAssets.from(rootDir.resolve("docs/static/icon-512-maskable.png")) homepageLink = Config.url } dokkaPublications.configureEach { diff --git a/buildSrc/src/main/kotlin/pro.respawn.android-library.gradle.kts b/buildSrc/src/main/kotlin/pro.respawn.android-library.gradle.kts new file mode 100644 index 0000000..36d0e87 --- /dev/null +++ b/buildSrc/src/main/kotlin/pro.respawn.android-library.gradle.kts @@ -0,0 +1,16 @@ +plugins { + kotlin("android") + id("com.android.library") +} + +kotlin { + explicitApi() +} + +android { + configureAndroidLibrary(this) + + kotlinOptions { + jvmTarget = Config.jvmTarget.target + } +} diff --git a/buildSrc/src/main/kotlin/pro.respawn.shared-library.gradle.kts b/buildSrc/src/main/kotlin/pro.respawn.shared-library.gradle.kts index 9beb915..c38b778 100644 --- a/buildSrc/src/main/kotlin/pro.respawn.shared-library.gradle.kts +++ b/buildSrc/src/main/kotlin/pro.respawn.shared-library.gradle.kts @@ -1,11 +1,9 @@ import org.gradle.kotlin.dsl.kotlin -import org.gradle.kotlin.dsl.signing import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi plugins { kotlin("multiplatform") id("com.android.library") - signing } kotlin { From f841022150c09b6828882c5079d66d3f0da9f211 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sun, 5 Jan 2025 15:43:26 +0100 Subject: [PATCH 12/14] update publishing workflow --- .github/changelog_config.json | 2 +- .github/workflows/publish.yml | 22 ++++++++++++++++++++-- buildSrc/src/main/kotlin/Config.kt | 2 +- gradle.properties | 2 +- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/.github/changelog_config.json b/.github/changelog_config.json index c53db87..2d8f82d 100644 --- a/.github/changelog_config.json +++ b/.github/changelog_config.json @@ -50,7 +50,7 @@ ], "label_extractor" : [ { - "pattern" : "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\\([\\w\\-\\.]+\\))?(!)?: ([\\w ])+([\\s\\S]*)", + "pattern" : "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test|feat!|breaking|api){1}(\\([\\w\\-\\.]+\\))?(!)?: ([\\w ])+([\\s\\S]*)", "target" : "$1" } ] diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9df5f8b..272c959 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,12 +21,13 @@ jobs: environment: publishing steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4 - name: Copy CI gradle.properties run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties - - name: set up JDK + - name: Set up JDK uses: actions/setup-java@v4 with: distribution: 'zulu' @@ -41,6 +42,22 @@ jobs: with: xcode-version: latest + - name: Create local properties + env: + LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }} + run: echo "$LOCAL_PROPERTIES" | base64 --decode > local.properties + + - name: Cache konan directory + uses: actions/cache@v4 + with: + path: ~/.konan + key: ${{ runner.os }}-konan-${{ hashFiles('*.gradle.kts', 'buildSrc/*') }} + restore-keys: | + ${{ runner.os }}-konan- + + - name: Assemble android sample + run: ./gradlew :app:assembleRelease + - name: Publish to sonatype env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USERNAME }} @@ -64,6 +81,7 @@ jobs: draft: true artifactErrorsFailBuild: true prerelease: false + artifacts: app/build/outputs/apk/release/* body: ${{steps.build_changelog.outputs.changelog}} tag: ${{ inputs.tag != '' && inputs.tag || github.ref_name }} env: diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index c91eb6a..0b897e7 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -25,6 +25,7 @@ object Config { const val licenseUrl = "http://www.apache.org/licenses/LICENSE-2.0.txt" const val scmUrl = "https://github.com/respawn-app/ApiResult.git" const val name = "ApiResult" + @Suppress("MaxLineLength") const val description = """ApiResult is a Kotlin Multiplatform declarative error handling library. Just like Arrow's Either, but without the complexity.""" const val supportEmail = "hello@respawn.pro" @@ -68,7 +69,6 @@ object Config { add("-Xjdk-release=${jvmTarget.target}") } - // android const val namespace = artifactId const val testRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/gradle.properties b/gradle.properties index c2f555b..b183749 100644 --- a/gradle.properties +++ b/gradle.properties @@ -39,7 +39,7 @@ kotlin.native.binary.gc=cms org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true org.gradle.configuration-cache.parallel=true -release=true +release=false #kotlin.kmp.isolated-projects.support=enable kotlin.incremental.wasm=true #org.gradle.unsafe.isolated-projects=true From c064ff2a93776207f291a425cd91114bd872e4fb Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sun, 5 Jan 2025 16:00:51 +0100 Subject: [PATCH 13/14] update readme --- README.md | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index de0f9d2..ce82506 100644 --- a/README.md +++ b/README.md @@ -17,20 +17,25 @@ ApiResult is [Railway Programming](https://proandroiddev.com/railway-oriented-pr functional error handling **on steroids**. -## Features +## Why use a library instead of try/catch? -* ApiResult is **lightweight**. The library creates no objects, makes no allocations or virtual function resolutions. - Most of the code is inlined. -* ApiResult offers 90+ operators covering most of possible use cases to turn your +Exceptions in Kotlin are **unchecked**. +Each time you call a function, it can throw and crash you app. +With ApiResult, you will never have this problem again. + +* ApiResult **forces** your code users to handle errors. Forget about unhandled exceptions and unexpected crashes. +* ApiResult is **lightweight**. The library creates no objects and has ~0 performance impact. +* Use 90+ operators covering most of possible use cases to turn your code from imperative and procedural to declarative and functional, which is more readable and extensible. -* ApiResult defines a contract that you can use in your code. No one will be able to obtain the result of a computation - without being forced to handle errors at compilation time. -* The library has 129 tests for 92% operator coverage. +* Core library has **no dependencies**. No need to worry about unexpected junk in your codebase. +* This isn't like Arrow, where with a monad you get a bunch of extra black magic. This framework focuses on **error handling** only. +* ApiResult is fully compatible with Exceptions and Coroutines. Just wrap a call and it will work. +* The library has 140+ tests for 92% operator coverage. Expect long-term support and stability. -## Preview +## How do I use it? ```kotlin -// wrap a result of a computation and expose the result +// wrap a result of any computation and expose the result class BillingRepository(private val api: RestApi) { suspend fun getSubscriptions() = ApiResult { @@ -38,18 +43,14 @@ class BillingRepository(private val api: RestApi) { } // -> ApiResult?> } -// ----- - // obtain and handle the result in the client code -val repo = BillingRepository( /* ... */) - fun onClickVerify() { - val state: SubscriptionState = repo.getSubscriptions() + val state: SubscriptionState = billingRepository.getSubscriptions() .errorOnNull() // map nulls to error states with compile-time safety .recover { emptyList() } // recover from some or all errors .require { securityRepository.isDeviceTrusted() } // conditionally fail the chain .mapValues(::SubscriptionModel) // map list items - .filter { it.isPurchased } // filter values + .filter { it.isPurchased } // filter .mapError { e -> BillingException(cause = e) } // map exceptions .then { validateSubscriptions(it) } // execute a computation and continue with its result, propagating errors .chain { updateGracePeriod(it) } // execute another computation, and if it fails, stop the chain @@ -92,7 +93,7 @@ Ready to try? Start with reading the [Quickstart Guide](https://opensource.respa ## License ``` - Copyright 2022-2024 Respawn Team and contributors + Copyright 2022-2025 Respawn Team and contributors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 9dbc4350291081cfc02719f1f6aac098c6a4bc9f Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sun, 5 Jan 2025 16:03:29 +0100 Subject: [PATCH 14/14] bump agp --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 31f6817..cd8763a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ dependencyAnalysisPlugin = "2.6.1" detekt = "1.23.7" detektFormattingPlugin = "1.23.7" dokka = "2.0.0" -gradleAndroid = "8.8.0-rc02" +gradleAndroid = "8.7.3" gradleDoctorPlugin = "0.10.0" kotest = "6.0.0.M1" kotlin = "2.1.0"