Move binding coroutine implementation to separate subproject

This commit is contained in:
Tristan Hamilton 2020-08-07 14:51:27 +01:00 committed by Michael Bull
parent ce0180f5cd
commit b16fb559a1
37 changed files with 403 additions and 161 deletions

View File

@ -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

View File

@ -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)
}
}
}
}

View File

@ -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")

View 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")
}
}
}
}

View File

@ -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
}
}
}
}

View File

@ -1,4 +1,4 @@
package com.github.michaelbull.result
package com.github.michaelbull.result.coroutines
import kotlinx.coroutines.CoroutineScope

View File

@ -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

View File

@ -1,4 +1,4 @@
package com.github.michaelbull.result
package com.github.michaelbull.result.coroutines
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.runBlocking

View File

@ -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
)
}
}
}

View 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")
}
}
}
}

View File

@ -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)

View File

@ -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()
}
}