diff --git a/.github/workflows/publish-pages-only.yml b/.github/workflows/publish-pages-only.yml index d8f9a63..542e251 100644 --- a/.github/workflows/publish-pages-only.yml +++ b/.github/workflows/publish-pages-only.yml @@ -17,14 +17,14 @@ jobs: - name: Checkout uses: actions/checkout@v3 - name: Build Dokka HTML - run: ./gradlew dokkaHtml + run: ./gradlew dokkaGenerate - name: Setup Pages uses: actions/configure-pages@v3 - name: Upload artifact uses: actions/upload-pages-artifact@v1 with: # Upload docs folder - path: './docs' + path: './docs/html' - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v2 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 58cd977..b04aae0 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -35,14 +35,14 @@ jobs: - name: Checkout uses: actions/checkout@v3 - name: Build Dokka HTML - run: ./gradlew dokkaHtml + run: ./gradlew dokkaGenerate - name: Setup Pages uses: actions/configure-pages@v3 - name: Upload artifact uses: actions/upload-pages-artifact@v1 with: # Upload docs folder - path: './docs' + path: './docs/html' - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b54735..6dd00d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,3 +44,15 @@ - add `recoverCatching` to match Kotlin's `result` - add `callsInPlace` contracts - return result fon `onFailure` and `onSuccess` + +## 1.8.0 +- migrate to dokka 2 for documentation +- multi-module project setup +- introduce `kmmresult-test`, featuring + - `result should succeed` + - `result shouldNot succeed` + - `result shouldSucceedWith expectedValue` + - `result.shouldSucceed()` returning the contained value +- remove Arrow dependency and import arrow's list of Fatal exceptions directly into our code +- Introduce `Result.nonFatalOrThrow` to mimic KmmResult's non-fatal-only behaviour, but without the object instantiation overhead +- Introduce `carchingUnwrapped`, which mimics KmmResult's non-fatal-only behaviour, but without the object instantiation overhead \ No newline at end of file diff --git a/README.md b/README.md index 8a74d61..38a76b4 100644 --- a/README.md +++ b/README.md @@ -47,14 +47,14 @@ var intResult = KmmResult.success(3) intResult = KmmResult.failure (NotImplementedError("Not Implemented")) ``` -Also provides `map()` to transform success types while passing through errors and `mapFailure` to transform error types -while passing through success cases. -In addition, the more generic `fold()` is available for conveniently operating on both success and failure branches. - - -There really is not much more to say, except for two things: - - `KmmResult` sports `unwrap()` to conveniently map it to the `kotlin.Result` equivalent - - It provides a `Result.wrap()` extension function to go the opposite way. +Convenience functions: +- `map()` transforms success types while passing through errors +- `mapFailure` transforms error types while passing through success cases +- the more generic `fold()` is available for conveniently operating on both success and failure branches +- `KmmResult` sports `unwrap()` to conveniently map it to the `kotlin.Result` equivalent +- `Result.wrap()` extension function goes the opposite way +- `mapCatching()` does what you'd expect +- `wrapping()` allows for wrapping the failure branch's exception unless it is of the specified type Refer to the [full documentation](https://a-sit-plus.github.io/kmmresult/) for more info. @@ -83,6 +83,18 @@ func funWithKotlin() -> KmmResult { } ``` +## Non-Fatal-Only `catching` +KmmResult comes with `catching`. This is a non-fatal-only-catching version of stdlib's `runCatching`, directly returning a `KmmResult`. +It re-throws any fatal exceptions, such as `OutOfMemoryError`. The underlying logic is borrowed from [Arrow's](https://arrow-kt.io)'s +[`nonFatalOrThrow`](https://apidocs.arrow-kt.io/arrow-core/arrow.core/non-fatal-or-throw.html). + +The only downside of `catching` is that it incurs instatiation overhead, because it creates a `KmmResult` instance. +Internally, though, only the behaviour is important, not Swift interop. Hence, you don't care for a `KmmResult` and you +certainly don't care for the cost of instantiating an object. Here, the `Result.nonFatalOrThrow()` extension shipped with KmmResult +comes to the rescue. It does exactly what the name suggest: It re-throws any fatal exception and leaved the `Result` object +untouched otherwise. + + Happy folding! ## Contributing diff --git a/build.gradle.kts b/build.gradle.kts index 4486921..9e14817 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,198 +1,40 @@ -import io.gitlab.arturbosch.detekt.Detekt import org.gradle.kotlin.dsl.support.listFilesOrdered -import org.jetbrains.kotlin.build.joinToReadableString -plugins { - kotlin("multiplatform") version "2.0.0" - id("maven-publish") - id("signing") +plugins {base + id("org.jetbrains.dokka") id("io.github.gradle-nexus.publish-plugin") version "1.3.0" - id("org.jetbrains.dokka") version "1.9.20" - id("org.jetbrains.kotlinx.kover") version "0.8.0" - id("io.gitlab.arturbosch.detekt") version "1.23.6" } val artifactVersion: String by extra group = "at.asitplus" version = artifactVersion -repositories { - mavenCentral() +dependencies { + dokka(project(":kmmresult")) + dokka(project(":kmmresult-test")) } -val dokkaOutputDir = "$projectDir/docs" -tasks.dokkaHtml { - +dokka { val moduleDesc = File("$rootDir/dokka-tmp.md").also { it.createNewFile() } val readme = File("${rootDir}/README.md").readText() - val moduleTitle = project.name - moduleDesc.writeText("# Module ${project.name}\n\n$readme") - moduleName.set(moduleTitle) - - dokkaSourceSets { - named("commonMain") { - - includes.from(moduleDesc) - sourceLink { - localDirectory.set(file("src/$name/kotlin")) - remoteUrl.set( - uri("https://github.com/a-sit-plus/kmmresult/tree/development/src/$name/kotlin").toURL() - ) - // Suffix which is used to append the line number to the URL. Use #L for GitHub - remoteLineSuffix.set("#L") - } - } - } - outputDirectory.set(file("${rootDir}/docs")) - doLast { - rootDir.listFilesOrdered { it.extension.lowercase() == "png" || it.extension.lowercase() == "svg" } - .forEach { it.copyTo(File("$rootDir/docs/${it.name}"), overwrite = true) } - } -} -val deleteDokkaOutputDir by tasks.register("deleteDokkaOutputDirectory") { - delete(dokkaOutputDir) -} -val javadocJar = tasks.register("javadocJar") { - dependsOn(deleteDokkaOutputDir, tasks.dokkaHtml) - archiveClassifier.set("javadoc") - from(dokkaOutputDir) -} + moduleDesc.writeText("\n\n$readme") + moduleName.set("KmmResult") -tasks.getByName("check") { - dependsOn("detektMetadataMain") -} - - -//first sign everything, then publish! -tasks.withType() { - tasks.withType().forEach { - dependsOn(it) + dokkaPublicationDirectory.set(file("${rootDir}/docs")) + dokkaPublications.html { + includes.from(moduleDesc) } } -kotlin { - - val xcf = org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFrameworkConfig(project, "KmmResult") - listOf( - macosArm64(), - macosX64(), - tvosArm64(), - tvosX64(), - tvosSimulatorArm64(), - iosX64(), - iosArm64(), - iosSimulatorArm64() - ).forEach { - it.binaries.framework { - baseName = "KmmResult" - binaryOption("bundleId", "at.asitplus.KmmResult") - embedBitcode("bitcode") - xcf.add(this) - isStatic = true - } - } - - jvmToolchain(11) - jvm { - compilations.all { - kotlinOptions { - freeCompilerArgs = listOf( - "-Xjsr305=strict" - ) - } - } - withJava() //for Java Interop tests - } - - js(IR) { - browser { testTask { enabled = false } } - nodejs() - } - linuxX64() - linuxArm64() - mingwX64() - - sourceSets { - commonMain.dependencies { - implementation("io.arrow-kt:arrow-core:1.2.4") - } - - commonTest.dependencies { - implementation(kotlin("test")) - } - } - - - - tasks.withType().configureEach { - reports { - xml.required.set(true) - html.required.set(false) - txt.required.set(false) - sarif.required.set(true) - md.required.set(true) - } - } - - dependencies { - detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.6") - } +tasks.dokkaGenerate { + doLast { + rootDir.listFilesOrdered { it.extension.lowercase() == "png" || it.extension.lowercase() == "svg" } + .forEach { it.copyTo(File("$rootDir/docs/html/${it.name}"), overwrite = true) } - repositories { - mavenCentral() - } - - publishing { - publications { - withType { - artifact(javadocJar) - pom { - name.set("KmmResult") - description.set("Functional equivalent of kotlin.Result but with KMM goodness") - url.set("https://github.com/a-sit-plus/kmmresult") - licenses { - license { - name.set("The Apache License, Version 2.0") - url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") - } - } - developers { - developer { - id.set("JesusMcCloud") - name.set("Bernd Prünster") - email.set("bernd.pruenster@a-sit.at") - } - developer { - id.set("nodh") - name.set("Christian Kollmann") - email.set("christian.kollmann@a-sit.at") - } - } - scm { - connection.set("scm:git:git@github.com:a-sit-plus/kmmresult.git") - developerConnection.set("scm:git:git@github.com:a-sit-plus/kmmresult.git") - url.set("https://github.com/a-sit-plus/kmmresult") - } - } - } - } - repositories { - mavenLocal() { - signing.isRequired = false - } - } } } -signing { - val signingKeyId: String? by project - val signingKey: String? by project - val signingPassword: String? by project - useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword) - sign(publishing.publications) -} - nexusPublishing { repositories { sonatype { @@ -201,4 +43,3 @@ nexusPublishing { } } } - diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 0000000..abb6a94 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + `kotlin-dsl` +} + +repositories { + mavenCentral() + gradlePluginPortal() +} + +dependencies { + implementation("org.jetbrains.dokka:dokka-gradle-plugin:2.0.0-Beta") + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.20") +} \ No newline at end of file diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle new file mode 100644 index 0000000..bd14ba4 --- /dev/null +++ b/buildSrc/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'kmmResult_buildSrc' diff --git a/gradle.properties b/gradle.properties index 33765e6..9dd40b5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,4 +3,5 @@ kotlin.mpp.enableCInteropCommonization=true kotlin.mpp.stability.nowarn=true kotlin.native.ignoreDisabledTargets=true -artifactVersion = 1.7.0 \ No newline at end of file +artifactVersion = 1.8.0 +org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled \ No newline at end of file diff --git a/kmmresult-test/build.gradle.kts b/kmmresult-test/build.gradle.kts new file mode 100644 index 0000000..4ed0908 --- /dev/null +++ b/kmmresult-test/build.gradle.kts @@ -0,0 +1,175 @@ +import io.gitlab.arturbosch.detekt.Detekt +import org.gradle.kotlin.dsl.support.listFilesOrdered +import java.lang.management.ManagementFactory +import java.net.URI + +plugins { + kotlin("multiplatform") + id("maven-publish") + id("signing") + id("org.jetbrains.dokka") + id("org.jetbrains.kotlinx.kover") version "0.8.0" + id("io.gitlab.arturbosch.detekt") version "1.23.6" +} + +val artifactVersion: String by extra +group = "at.asitplus" +version = artifactVersion + +repositories { + mavenCentral() +} + +val dokkaOutputDir = "$projectDir/docs" +dokka { + dokkaSourceSets { + + named("commonMain") { + sourceLink { + val path = "${projectDir}/src/$name/kotlin" + println(path) + localDirectory.set(file(path)) + remoteUrl.set( + URI("https://github.com/a-sit-plus/kmmresult/tree/main/src/$name/kotlin") + ) + // Suffix which is used to append the line number to the URL. Use #L for GitHub + remoteLineSuffix.set("#L") + } + } + } + pluginsConfiguration.html { + footerMessage = "© 2024 A-SIT Plus GmbH" + } +} +val deleteDokkaOutputDir by tasks.register("deleteDokkaOutputDirectory") { + delete(dokkaOutputDir) +} +val javadocJar = tasks.register("javadocJar") { + dependsOn(deleteDokkaOutputDir, tasks.dokkaGenerate) + archiveClassifier.set("javadoc") + from(dokkaOutputDir) +} + +tasks.getByName("check") { + dependsOn("detektMetadataMain") +} + + +//first sign everything, then publish! +tasks.withType() { + tasks.withType().forEach { + dependsOn(it) + } +} + +kotlin { + + macosArm64() + macosX64() + tvosArm64() + tvosX64() + tvosSimulatorArm64() + iosX64() + iosArm64() + iosSimulatorArm64() + + + jvmToolchain(11) + jvm { + compilations.all { + kotlinOptions { + freeCompilerArgs = listOf( + "-Xjsr305=strict" + ) + } + } + withJava() //for Java Interop tests + } + + js(IR) { + browser { testTask { enabled = false } } + nodejs() + } + linuxX64() + linuxArm64() + mingwX64() + + sourceSets { + commonMain.dependencies { + implementation(project(":kmmresult")) + api("io.kotest:kotest-assertions-core:5.9.1") + } + } + + tasks.withType().configureEach { + reports { + xml.required.set(true) + html.required.set(false) + txt.required.set(false) + sarif.required.set(true) + md.required.set(true) + } + } +} + + + + +dependencies { + detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.6") + } + + repositories { + mavenCentral() + } + + publishing { + publications { + withType { + artifact(javadocJar) + pom { + name.set("KmmResult Test") + description.set("Kotest helperrs for KmmResult") + url.set("https://github.com/a-sit-plus/kmmresult") + licenses { + license { + name.set("The Apache License, Version 2.0") + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + } + } + developers { + developer { + id.set("JesusMcCloud") + name.set("Bernd Prünster") + email.set("bernd.pruenster@a-sit.at") + } + developer { + id.set("nodh") + name.set("Christian Kollmann") + email.set("christian.kollmann@a-sit.at") + } + } + scm { + connection.set("scm:git:git@github.com:a-sit-plus/kmmresult.git") + developerConnection.set("scm:git:git@github.com:a-sit-plus/kmmresult.git") + url.set("https://github.com/a-sit-plus/kmmresult") + } + } + } + } + repositories { + mavenLocal() { + signing.isRequired = false + } + } + } + + +signing { + val signingKeyId: String? by project + val signingKey: String? by project + val signingPassword: String? by project + useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword) + sign(publishing.publications) +} + diff --git a/kmmresult-test/src/commonMain/kotlin/at/asitplus/TestExtensions.kt b/kmmresult-test/src/commonMain/kotlin/at/asitplus/TestExtensions.kt new file mode 100644 index 0000000..3de0bc6 --- /dev/null +++ b/kmmresult-test/src/commonMain/kotlin/at/asitplus/TestExtensions.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2021 - 2023 A-SIT Plus GmbH. Obviously inspired and partially copy-pasted from kotlin.Result. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ +package at.asitplus + +import io.kotest.matchers.Matcher +import io.kotest.matchers.MatcherResult +import io.kotest.matchers.should +import io.kotest.matchers.shouldBe + +/** + * Shorthand for `getOrThrow() shouldBe expected` + */ +infix fun KmmResult.shouldSucceedWith(expected: T): T = getOrThrow() shouldBe expected + +/** + * [KmmResult] matcher. Use as follows: `okResult should succeed`, `errResult shouldNot succeed` + */ +@Suppress("ClassNaming") +object succeed : Matcher> { + override fun test(value: KmmResult<*>) = + MatcherResult( + value.isSuccess, + failureMessageFn = { + "Should have succeeded, but failed:\n${ + value.exceptionOrNull()!!.stackTraceToString() + }" + }, + negatedFailureMessageFn = { "Should have failed, but succeeded with ${value.getOrNull()!!}" } + ) +} + +/** + * Asserts that this KmmResult should succeed and returns the contained value + */ +fun KmmResult.shouldSucceed(): T { + this should succeed + return getOrThrow() +} diff --git a/kmmresult/build.gradle.kts b/kmmresult/build.gradle.kts new file mode 100644 index 0000000..ceeea2a --- /dev/null +++ b/kmmresult/build.gradle.kts @@ -0,0 +1,188 @@ +import io.gitlab.arturbosch.detekt.Detekt +import java.lang.management.ManagementFactory +import java.net.URI + +plugins { + kotlin("multiplatform") + id("maven-publish") + id("signing") + id("org.jetbrains.dokka") + id("org.jetbrains.kotlinx.kover") version "0.8.0" + id("io.gitlab.arturbosch.detekt") version "1.23.6" +} + +val artifactVersion: String by extra +group = "at.asitplus" +version = artifactVersion + +repositories { + mavenCentral() +} + +val dokkaOutputDir = "$projectDir/docs" +dokka { + dokkaSourceSets { + + named("commonMain") { + sourceLink { + val path = "${projectDir}/src/$name/kotlin" + println(path) + localDirectory.set(file(path)) + remoteUrl.set( + URI("https://github.com/a-sit-plus/kmmresult/tree/main/src/$name/kotlin") + ) + // Suffix which is used to append the line number to the URL. Use #L for GitHub + remoteLineSuffix.set("#L") + } + } + } + pluginsConfiguration.html { + footerMessage = "© 2024 A-SIT Plus GmbH" + } +} + + +val deleteDokkaOutputDir by tasks.register("deleteDokkaOutputDirectory") { + delete(dokkaOutputDir) +} +val javadocJar = tasks.register("javadocJar") { + dependsOn(deleteDokkaOutputDir, tasks.dokkaGenerate) + archiveClassifier.set("javadoc") + from(dokkaOutputDir) +} + +tasks.getByName("check") { + dependsOn("detektMetadataMain") +} + + +//first sign everything, then publish! +tasks.withType() { + tasks.withType().forEach { + dependsOn(it) + } +} + +kotlin { + + val xcf = org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFrameworkConfig(project, "KmmResult") + listOf( + macosArm64(), + macosX64(), + tvosArm64(), + tvosX64(), + tvosSimulatorArm64(), + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { + it.binaries.framework { + baseName = "KmmResult" + binaryOption("bundleId", "at.asitplus.KmmResult") + embedBitcode("bitcode") + xcf.add(this) + isStatic = true + } + } + + jvmToolchain(11) + jvm { + compilations.all { + kotlinOptions { + freeCompilerArgs = listOf( + "-Xjsr305=strict" + ) + } + } + withJava() //for Java Interop tests + } + + js(IR) { + browser { testTask { enabled = false } } + nodejs() + } + + linuxX64() + linuxArm64() + mingwX64() + + sourceSets { + commonTest.dependencies { + implementation(kotlin("test")) + } + } + sourceSets.filterNot { it.name.startsWith("common") || it.name.startsWith("jvm") }.filter { it.name.endsWith("Main") }.forEach {srcSet-> + println(srcSet.name) + srcSet.kotlin.srcDir("$projectDir/src/nonJvmMain/kotlin") + } +} + + + tasks.withType().configureEach { + reports { + xml.required.set(true) + html.required.set(false) + txt.required.set(false) + sarif.required.set(true) + md.required.set(true) + } + } + + dependencies { + detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.6") + } + + repositories { + mavenCentral() + } + + publishing { + publications { + withType { + artifact(javadocJar) + pom { + name.set("KmmResult") + description.set("Functional equivalent of kotlin.Result but with KMM goodness") + url.set("https://github.com/a-sit-plus/kmmresult") + licenses { + license { + name.set("The Apache License, Version 2.0") + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + } + } + developers { + developer { + id.set("JesusMcCloud") + name.set("Bernd Prünster") + email.set("bernd.pruenster@a-sit.at") + } + developer { + id.set("nodh") + name.set("Christian Kollmann") + email.set("christian.kollmann@a-sit.at") + } + } + scm { + connection.set("scm:git:git@github.com:a-sit-plus/kmmresult.git") + developerConnection.set("scm:git:git@github.com:a-sit-plus/kmmresult.git") + url.set("https://github.com/a-sit-plus/kmmresult") + } + } + } + } + repositories { + mavenLocal() { + signing.isRequired = false + } + } + } + + +signing { + val signingKeyId: String? by project + val signingKey: String? by project + val signingPassword: String? by project + useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword) + sign(publishing.publications) +} + diff --git a/src/commonMain/kotlin/at/asitplus/KmmResult.kt b/kmmresult/src/commonMain/kotlin/at/asitplus/KmmResult.kt similarity index 97% rename from src/commonMain/kotlin/at/asitplus/KmmResult.kt rename to kmmresult/src/commonMain/kotlin/at/asitplus/KmmResult.kt index d2a4249..6c59566 100644 --- a/src/commonMain/kotlin/at/asitplus/KmmResult.kt +++ b/kmmresult/src/commonMain/kotlin/at/asitplus/KmmResult.kt @@ -7,7 +7,6 @@ package at.asitplus -import arrow.core.nonFatalOrThrow import kotlin.contracts.InvocationKind import kotlin.contracts.contract import kotlin.experimental.ExperimentalObjCName @@ -235,6 +234,7 @@ private constructor( @OptIn(ExperimentalObjCRefinement::class) companion object { + @HiddenFromObjC @JvmStatic fun success(value: T): KmmResult = KmmResult(value) @@ -267,8 +267,9 @@ inline fun KmmResult.recoverCatching(block: (error: Throwable) -> /** * Non-fatal-only-catching version of stdlib's [runCatching], directly returning a [KmmResult] -- - * Re-throws any fatal exceptions, such as `OutOfMemoryError`. Relies on [Arrow](https://arrow-kt.io)'s - * [nonFatalOrThrow](https://apidocs.arrow-kt.io/arrow-core/arrow.core/non-fatal-or-throw.html) internally. + * Re-throws any fatal exceptions, such as `OutOfMemoryError`. Re-implements [Arrow](https://arrow-kt.io)'s + * [nonFatalOrThrow](https://apidocs.arrow-kt.io/arrow-core/arrow.core/non-fatal-or-throw.html) + * logic to avoid a dependency on Arrow for a single function. */ @Suppress("TooGenericExceptionCaught") inline fun catching(block: () -> T): KmmResult { @@ -286,8 +287,9 @@ inline fun catching(block: () -> T): KmmResult { /** * Non-fatal-only-catching version of stdlib's [runCatching] (calling the specified function [block] with `this` value * as its receiver), directly returning a [KmmResult] -- - * Re-throws any fatal exceptions, such as `OutOfMemoryError`. Relies on [Arrow](https://arrow-kt.io)'s - * [nonFatalOrThrow](https://apidocs.arrow-kt.io/arrow-core/arrow.core/non-fatal-or-throw.html) internally. + * Re-throws any fatal exceptions, such as `OutOfMemoryError`. Re-implements [Arrow](https://arrow-kt.io)'s + * [nonFatalOrThrow](https://apidocs.arrow-kt.io/arrow-core/arrow.core/non-fatal-or-throw.html) + * logic to avoid a dependency on Arrow for a single function. */ @Suppress("TooGenericExceptionCaught") inline fun T.catching(block: T.() -> R): KmmResult { diff --git a/kmmresult/src/commonMain/kotlin/at/asitplus/NonFatal.kt b/kmmresult/src/commonMain/kotlin/at/asitplus/NonFatal.kt new file mode 100644 index 0000000..767bee6 --- /dev/null +++ b/kmmresult/src/commonMain/kotlin/at/asitplus/NonFatal.kt @@ -0,0 +1,37 @@ +package at.asitplus + +/** + * Throws any fatal exceptions. This is a re-implementation taken from Arrow's + * [`nonFatalOrThrow`](https://apidocs.arrow-kt.io/arrow-core/arrow.core/non-fatal-or-throw.html) – + * to avoid a dependency on Arrow for a single function. + */ +@Suppress("NOTHING_TO_INLINE") +expect inline fun Throwable.nonFatalOrThrow(): Throwable + +/** + * Helper to effectively convert stdlib's [runCatching] to behave like KmmResult's Non-fatal-only [catching]. I.e. any + * fatal exceptions are thrown. + * The reason this exists is that [catching] incurs instantiation cost. + * This helper hence provides the best of both worlds. + */ +@Suppress("NOTHING_TO_INLINE") +inline fun Result.nonFatalOrThrow(): Result = this.onFailure { it.nonFatalOrThrow() } + +/** + * Non-fatal-only-catching version of stdlib's [runCatching], returning a [Result] -- + * Re-throws any fatal exceptions, such as `OutOfMemoryError`. Re-implements [Arrow](https://arrow-kt.io)'s + * [nonFatalOrThrow](https://apidocs.arrow-kt.io/arrow-core/arrow.core/non-fatal-or-throw.html) + * logic to avoid a dependency on Arrow for a single function. + */ +@Suppress("NOTHING_TO_INLINE") +inline fun catchingUnwrapped(block: () -> T): Result = runCatching(block).nonFatalOrThrow() + +/** + * Non-fatal-only-catching version of stdlib's [runCatching] (calling the specified function [block] with `this` value + * as its receiver), directly returning a [Result] -- + * Re-throws any fatal exceptions, such as `OutOfMemoryError`. Re-implements [Arrow](https://arrow-kt.io)'s + * [nonFatalOrThrow](https://apidocs.arrow-kt.io/arrow-core/arrow.core/non-fatal-or-throw.html) + * logic to avoid a dependency on Arrow for a single function. + */ +@Suppress("NOTHING_TO_INLINE") +inline fun T.catchingUnwrapped(block: T.() -> R): Result = runCatching(block).nonFatalOrThrow() diff --git a/src/commonTest/kotlin/KmmResultTest.kt b/kmmresult/src/commonTest/kotlin/KmmResultTest.kt similarity index 93% rename from src/commonTest/kotlin/KmmResultTest.kt rename to kmmresult/src/commonTest/kotlin/KmmResultTest.kt index ae0222f..507c914 100644 --- a/src/commonTest/kotlin/KmmResultTest.kt +++ b/kmmresult/src/commonTest/kotlin/KmmResultTest.kt @@ -216,4 +216,21 @@ class KmmResultTest { result.transform { catching { fn(it) }} } } + + @Test + fun testNonFatal() { + + runCatching { throw CancellationException() } + + assertFailsWith(CancellationException::class) { + runCatching { throw CancellationException() }.nonFatalOrThrow() + } + assertFailsWith(CancellationException::class) { + catching { throw CancellationException() } + } + + runCatching { throw IndexOutOfBoundsException() }.nonFatalOrThrow() + catching { throw IndexOutOfBoundsException() } + + } } diff --git a/kmmresult/src/jvmMain/kotlin/at/asitplus/NonFatal.jvm.kt b/kmmresult/src/jvmMain/kotlin/at/asitplus/NonFatal.jvm.kt new file mode 100644 index 0000000..5deae44 --- /dev/null +++ b/kmmresult/src/jvmMain/kotlin/at/asitplus/NonFatal.jvm.kt @@ -0,0 +1,9 @@ +package at.asitplus + +import kotlin.coroutines.cancellation.CancellationException +//Taken from Arrow: https://github.com/arrow-kt/arrow/blob/99de6148320a4299a5aef20686a6063ca732026b/arrow-libs/core/arrow-core/src/jvmMain/kotlin/arrow/core/NonFatal.kt +@Suppress("NOTHING_TO_INLINE") +actual inline fun Throwable.nonFatalOrThrow(): Throwable = when (this) { + is VirtualMachineError, is ThreadDeath, is InterruptedException, is LinkageError, is CancellationException -> throw this + else -> this +} diff --git a/src/jvmTest/java/JavaInteropTests.java b/kmmresult/src/jvmTest/java/JavaInteropTests.java similarity index 100% rename from src/jvmTest/java/JavaInteropTests.java rename to kmmresult/src/jvmTest/java/JavaInteropTests.java diff --git a/kmmresult/src/nonJvmMain/kotlin/at/asitplus/NonFatal.nonJvm.kt b/kmmresult/src/nonJvmMain/kotlin/at/asitplus/NonFatal.nonJvm.kt new file mode 100644 index 0000000..5283ea5 --- /dev/null +++ b/kmmresult/src/nonJvmMain/kotlin/at/asitplus/NonFatal.nonJvm.kt @@ -0,0 +1,9 @@ +package at.asitplus + +import kotlin.coroutines.cancellation.CancellationException +//Taken from Arrow: https://github.com/arrow-kt/arrow/blob/99de6148320a4299a5aef20686a6063ca732026b/arrow-libs/core/arrow-core/src/nonJvmMain/kotlin/arrow/core/NonFatal.kt +@Suppress("NOTHING_TO_INLINE") +actual inline fun Throwable.nonFatalOrThrow(): Throwable = when (this) { + is CancellationException -> throw this + else -> this +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 22c130a..1fa33e5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,2 +1,20 @@ +rootProject.name = "kmmresult-root" //DOKKA BUG with spaces -rootProject.name = "kmmresult" \ No newline at end of file + +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } +} + +@Suppress("UnstableApiUsage") +dependencyResolutionManagement { + repositories { + mavenCentral() + } +} + + +include("kmmresult") +include("kmmresult-test") \ No newline at end of file