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/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/.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..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
@@ -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/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 518a010..272c959 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -21,17 +21,18 @@ 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'
check-latest: true
- java-version: 22
+ java-version: 23
cache: 'gradle'
- name: Validate gradle wrapper
@@ -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/.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/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.
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/Config.kt b/buildSrc/src/main/kotlin/Config.kt
index 982c20e..0b897e7 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,9 @@ 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,32 +35,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
const val testRunner = "androidx.test.runner.AndroidJUnitRunner"
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
new file mode 100644
index 0000000..e2905fa
--- /dev/null
+++ b/buildSrc/src/main/kotlin/dokkaDocumentation.gradle.kts
@@ -0,0 +1,38 @@
+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}"
+ customAssets.from(rootDir.resolve("docs/static/icon-512-maskable.png"))
+ 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/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 {
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)
}
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/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
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)
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/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..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()
@@ -28,8 +27,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
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
+ }
+ }
+ }
+})
diff --git a/gradle.properties b/gradle.properties
index 09a2657..b183749 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
+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=false
+#kotlin.kmp.isolated-projects.support=enable
+kotlin.incremental.wasm=true
+#org.gradle.unsafe.isolated-projects=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 10180ad..cd8763a 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.7.3"
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 = [
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 2c35211..a4b76b9 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
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