Move binding coroutine implementation to separate subproject
This commit is contained in:
parent
ce0180f5cd
commit
b16fb559a1
30
README.md
30
README.md
@ -136,6 +136,36 @@ resources on the topic of monad comprehensions.
|
||||
- [Monad comprehensions - Bow (Swift)][bow-monad-comprehension]
|
||||
- [For comprehensions - Scala][scala-for-comprehension]
|
||||
|
||||
#### Coroutine Support
|
||||
|
||||
Use of coroutines within a `binding` block requires an additional dependency:
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
implementation("com.michael-bull.kotlin-result:kotlin-result:1.1.8")
|
||||
implementation("com.michael-bull.kotlin-result:kotlin-result-coroutines:1.1.8")
|
||||
}
|
||||
```
|
||||
|
||||
This allows for asynchronous binds to operate so that if a bind were to fail,
|
||||
the binding block will return with the first failing async result:
|
||||
|
||||
```kotlin
|
||||
|
||||
suspend fun failsIn5ms(): Result<Int, DomainErrorA> { ... }
|
||||
suspend fun failsIn1ms(): Result<Int, DomainErrorB> { ... }
|
||||
|
||||
runBlocking{
|
||||
val result = binding<Int, BindingError> {
|
||||
val x = async { failsIn5ms().bind() }
|
||||
val y = async { failsIn1ms().bind() }
|
||||
x.await() + y.await()
|
||||
}
|
||||
|
||||
// result will be Err(DomainErrorB)
|
||||
}
|
||||
```
|
||||
|
||||
## Inspiration
|
||||
|
||||
Inspiration for this library has been drawn from other languages in which the
|
||||
|
127
build.gradle.kts
127
build.gradle.kts
@ -1,5 +1,7 @@
|
||||
import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
|
||||
import org.jetbrains.dokka.gradle.DokkaTask
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper
|
||||
|
||||
val ossrhUsername: String? by ext
|
||||
val ossrhPassword: String? by ext
|
||||
@ -7,14 +9,14 @@ val ossrhPassword: String? by ext
|
||||
description = "A Result monad for modelling success or failure operations."
|
||||
|
||||
plugins {
|
||||
`maven-publish`
|
||||
signing
|
||||
kotlin("multiplatform") version "1.3.72"
|
||||
id("org.jetbrains.dokka") version "0.10.1"
|
||||
base
|
||||
id("com.github.ben-manes.versions") version "0.28.0"
|
||||
id("net.researchgate.release") version "2.8.1"
|
||||
id("kotlinx.benchmark") version "0.2.0-dev-8"
|
||||
id("org.jetbrains.kotlin.plugin.allopen") version "1.3.72"
|
||||
|
||||
kotlin("multiplatform") version "1.3.72" apply false
|
||||
id("kotlinx.benchmark") version "0.2.0-dev-8" apply false
|
||||
id("net.researchgate.release") version "2.8.1" apply false
|
||||
id("org.jetbrains.dokka") version "0.10.1" apply false
|
||||
id("org.jetbrains.kotlin.plugin.allopen") version "1.3.72" apply false
|
||||
}
|
||||
|
||||
tasks.withType<DependencyUpdatesTask> {
|
||||
@ -25,19 +27,6 @@ tasks.withType<DependencyUpdatesTask> {
|
||||
}
|
||||
}
|
||||
|
||||
val dokka by tasks.existing(DokkaTask::class) {
|
||||
outputFormat = "javadoc"
|
||||
outputDirectory = "$buildDir/docs/javadoc"
|
||||
}
|
||||
|
||||
val javadocJar by tasks.registering(Jar::class) {
|
||||
group = LifecycleBasePlugin.BUILD_GROUP
|
||||
description = "Assembles a jar archive containing the Javadoc API documentation."
|
||||
archiveClassifier.set("javadoc")
|
||||
dependsOn(dokka)
|
||||
from(dokka.get().outputDirectory)
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@ -46,78 +35,44 @@ allprojects {
|
||||
}
|
||||
}
|
||||
|
||||
allOpen {
|
||||
annotation("org.openjdk.jmh.annotations.State")
|
||||
annotation("org.openjdk.jmh.annotations.BenchmarkMode")
|
||||
}
|
||||
subprojects {
|
||||
plugins.withType<MavenPublishPlugin> {
|
||||
apply(plugin = "net.researchgate.release")
|
||||
apply(plugin = "org.gradle.signing")
|
||||
|
||||
sourceSets.create("benchmark")
|
||||
val afterReleaseBuild by tasks.existing(DefaultTask::class)
|
||||
val publish by tasks.existing(Task::class)
|
||||
|
||||
benchmark {
|
||||
targets {
|
||||
register("jvmBenchmark")
|
||||
afterReleaseBuild {
|
||||
dependsOn(publish)
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
plugins.withType<KotlinMultiplatformPluginWrapper> {
|
||||
apply(plugin = "org.jetbrains.dokka")
|
||||
|
||||
val dokka by tasks.existing(DokkaTask::class) {
|
||||
outputFormat = "javadoc"
|
||||
outputDirectory = "$buildDir/docs/javadoc"
|
||||
}
|
||||
|
||||
val javadocJar by tasks.registering(Jar::class) {
|
||||
group = LifecycleBasePlugin.BUILD_GROUP
|
||||
description = "Assembles a jar archive containing the Javadoc API documentation."
|
||||
archiveClassifier.set("javadoc")
|
||||
dependsOn(dokka)
|
||||
from(dokka.get().outputDirectory)
|
||||
}
|
||||
|
||||
configure<KotlinMultiplatformExtension> {
|
||||
jvm {
|
||||
withJava()
|
||||
|
||||
mavenPublication {
|
||||
artifact(javadocJar.get())
|
||||
}
|
||||
|
||||
compilations.all {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
all {
|
||||
languageSettings.apply {
|
||||
useExperimentalAnnotation("kotlin.contracts.ExperimentalContracts")
|
||||
}
|
||||
}
|
||||
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("stdlib-common"))
|
||||
}
|
||||
}
|
||||
|
||||
val commonTest by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("test-common"))
|
||||
implementation(kotlin("test-annotations-common"))
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-native:1.3.7")
|
||||
}
|
||||
}
|
||||
|
||||
val jvmMain by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("stdlib-jdk8"))
|
||||
}
|
||||
}
|
||||
|
||||
val jvmTest by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("test-junit"))
|
||||
implementation(kotlin("test"))
|
||||
}
|
||||
}
|
||||
|
||||
val jvmBenchmark by getting {
|
||||
dependsOn(jvmMain)
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlinx:kotlinx.benchmark.runtime-jvm:0.2.0-dev-8")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
publishing {
|
||||
configure<PublishingExtension> {
|
||||
repositories {
|
||||
maven {
|
||||
if (project.version.toString().endsWith("SNAPSHOT")) {
|
||||
@ -188,13 +143,11 @@ publishing {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signing {
|
||||
configure<SigningExtension> {
|
||||
useGpgCmd()
|
||||
sign(publishing.publications)
|
||||
}
|
||||
|
||||
tasks.afterReleaseBuild {
|
||||
dependsOn(tasks.publish)
|
||||
sign(publications)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ repositories {
|
||||
dependencies {
|
||||
val ktorVersion = "1.3.2"
|
||||
|
||||
implementation(rootProject)
|
||||
implementation(project(":kotlin-result"))
|
||||
implementation(kotlin("stdlib-jdk8"))
|
||||
implementation("ch.qos.logback:logback-classic:1.2.3")
|
||||
implementation("io.ktor:ktor-server-core:$ktorVersion")
|
||||
|
41
kotlin-result-coroutines/build.gradle.kts
Normal file
41
kotlin-result-coroutines/build.gradle.kts
Normal file
@ -0,0 +1,41 @@
|
||||
description = "Extensions for using kotlin-result with kotlinx-coroutines."
|
||||
|
||||
plugins {
|
||||
`maven-publish`
|
||||
kotlin("multiplatform")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
all {
|
||||
languageSettings.apply {
|
||||
useExperimentalAnnotation("kotlin.contracts.ExperimentalContracts")
|
||||
}
|
||||
}
|
||||
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("stdlib-common"))
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-native:1.3.8")
|
||||
implementation(project(":kotlin-result"))
|
||||
}
|
||||
}
|
||||
|
||||
val commonTest by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("test-common"))
|
||||
implementation(kotlin("test-annotations-common"))
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-native:1.3.8")
|
||||
}
|
||||
}
|
||||
|
||||
val jvmTest by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("test-junit"))
|
||||
implementation(kotlin("test"))
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,54 @@
|
||||
package com.github.michaelbull.result.coroutines.binding
|
||||
|
||||
import com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.Ok
|
||||
import com.github.michaelbull.result.Result
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
||||
/**
|
||||
* Suspending variant of [binding][com.github.michaelbull.result.binding].
|
||||
*/
|
||||
suspend inline fun <V, E> binding(crossinline block: suspend SuspendableResultBinding<E>.() -> V): Result<V, E> {
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
|
||||
val receiver = SuspendableResultBindingImpl<E>()
|
||||
|
||||
return try {
|
||||
with(receiver) { Ok(block()) }
|
||||
} catch (ex: BindCancellationException) {
|
||||
receiver.internalError
|
||||
}
|
||||
}
|
||||
|
||||
internal object BindCancellationException : CancellationException(null)
|
||||
|
||||
interface SuspendableResultBinding<E> {
|
||||
suspend fun <V> Result<V, E>.bind(): V
|
||||
}
|
||||
|
||||
@PublishedApi
|
||||
internal class SuspendableResultBindingImpl<E> : SuspendableResultBinding<E> {
|
||||
|
||||
private val mutex = Mutex()
|
||||
lateinit var internalError: Err<E>
|
||||
|
||||
override suspend fun <V> Result<V, E>.bind(): V {
|
||||
return when (this) {
|
||||
is Ok -> value
|
||||
is Err -> {
|
||||
mutex.withLock {
|
||||
if (::internalError.isInitialized.not()){
|
||||
internalError = this
|
||||
}
|
||||
}
|
||||
throw BindCancellationException
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.github.michaelbull.result
|
||||
package com.github.michaelbull.result.coroutines
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -1,9 +1,9 @@
|
||||
package com.github.michaelbull.result.coroutines
|
||||
package com.github.michaelbull.result.coroutines.binding
|
||||
|
||||
import com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.Ok
|
||||
import com.github.michaelbull.result.Result
|
||||
import com.github.michaelbull.result.runBlockingTest
|
||||
import com.github.michaelbull.result.coroutines.runBlockingTest
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
@ -1,4 +1,4 @@
|
||||
package com.github.michaelbull.result
|
||||
package com.github.michaelbull.result.coroutines
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.runBlocking
|
@ -0,0 +1,79 @@
|
||||
package com.github.michaelbull.result.coroutines.binding
|
||||
|
||||
import com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.Ok
|
||||
import com.github.michaelbull.result.Result
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class AsyncSuspendableBindingTest {
|
||||
|
||||
private sealed class BindingError {
|
||||
object BindingErrorA : BindingError()
|
||||
object BindingErrorB : BindingError()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun returnsOkIfAllBindsSuccessful() {
|
||||
suspend fun provideX(): Result<Int, BindingError> {
|
||||
delay(100)
|
||||
return Ok(1)
|
||||
}
|
||||
|
||||
suspend fun provideY(): Result<Int, BindingError> {
|
||||
delay(100)
|
||||
return Ok(2)
|
||||
}
|
||||
|
||||
runBlocking {
|
||||
val result = binding<Int, BindingError> {
|
||||
val x = async { provideX().bind() }
|
||||
val y = async { provideY().bind() }
|
||||
x.await() + y.await()
|
||||
}
|
||||
|
||||
assertTrue(result is Ok)
|
||||
assertEquals(
|
||||
expected = 3,
|
||||
actual = result.value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun returnsFirstErrIfBindingFailed() {
|
||||
suspend fun provideX(): Result<Int, BindingError> {
|
||||
delay(1)
|
||||
return Ok(1)
|
||||
}
|
||||
|
||||
suspend fun provideY(): Result<Int, BindingError.BindingErrorA> {
|
||||
delay(2)
|
||||
return Err(BindingError.BindingErrorA)
|
||||
}
|
||||
|
||||
suspend fun provideZ(): Result<Int, BindingError.BindingErrorB> {
|
||||
delay(1)
|
||||
return Err(BindingError.BindingErrorB)
|
||||
}
|
||||
|
||||
runBlocking {
|
||||
val result = binding<Int, BindingError> {
|
||||
val x = async { provideX().bind() }
|
||||
val y = async { provideY().bind() }
|
||||
val z = async { provideZ().bind() }
|
||||
x.await() + y.await() + z.await()
|
||||
}
|
||||
|
||||
assertTrue(result is Err)
|
||||
assertEquals(
|
||||
expected = BindingError.BindingErrorB,
|
||||
actual = result.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
75
kotlin-result/build.gradle.kts
Normal file
75
kotlin-result/build.gradle.kts
Normal file
@ -0,0 +1,75 @@
|
||||
description = "A Result monad for modelling success or failure operations."
|
||||
|
||||
plugins {
|
||||
`maven-publish`
|
||||
kotlin("multiplatform")
|
||||
id("org.jetbrains.kotlin.plugin.allopen")
|
||||
id("kotlinx.benchmark")
|
||||
}
|
||||
|
||||
allOpen {
|
||||
annotation("org.openjdk.jmh.annotations.State")
|
||||
annotation("org.openjdk.jmh.annotations.BenchmarkMode")
|
||||
}
|
||||
|
||||
sourceSets.create("benchmark")
|
||||
|
||||
benchmark {
|
||||
targets {
|
||||
register("jvmBenchmark")
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvm {
|
||||
withJava()
|
||||
|
||||
compilations.all {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
all {
|
||||
languageSettings.apply {
|
||||
useExperimentalAnnotation("kotlin.contracts.ExperimentalContracts")
|
||||
}
|
||||
}
|
||||
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("stdlib-common"))
|
||||
}
|
||||
}
|
||||
|
||||
val commonTest by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("test-common"))
|
||||
implementation(kotlin("test-annotations-common"))
|
||||
}
|
||||
}
|
||||
|
||||
val jvmMain by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("stdlib-jdk8"))
|
||||
}
|
||||
}
|
||||
|
||||
val jvmTest by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("test-junit"))
|
||||
implementation(kotlin("test"))
|
||||
}
|
||||
}
|
||||
|
||||
val jvmBenchmark by getting {
|
||||
dependsOn(jvmMain)
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlinx:kotlinx.benchmark.runtime-jvm:0.2.0-dev-8")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,12 @@ import kotlin.contracts.contract
|
||||
/**
|
||||
* Suspending variant of [binding][com.github.michaelbull.result.binding].
|
||||
*/
|
||||
@Deprecated(
|
||||
message = "Will throw a runtime exception if used with async requests that fail to bind. " +
|
||||
"See https://github.com/michaelbull/kotlin-result/pull/28 " +
|
||||
"Please import the kotlin-result-coroutines library to continue using this feature.",
|
||||
level = DeprecationLevel.WARNING
|
||||
)
|
||||
suspend inline fun <V, E> binding(crossinline block: suspend ResultBinding<E>.() -> V): Result<V, E> {
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
@ -1,10 +1,14 @@
|
||||
rootProject.name = "kotlin-result"
|
||||
|
||||
include("example")
|
||||
include(
|
||||
"example",
|
||||
"kotlin-result",
|
||||
"kotlin-result-coroutines"
|
||||
)
|
||||
|
||||
pluginManagement {
|
||||
repositories {
|
||||
maven("https://dl.bintray.com/kotlin/kotlinx" )
|
||||
maven("https://dl.bintray.com/kotlin/kotlinx")
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user