From bd7e1244b31e59e7d26479d7673d24716f70be0d Mon Sep 17 00:00:00 2001 From: Tristan Hamilton Date: Fri, 17 Jul 2020 16:43:41 +0100 Subject: [PATCH] Add suspend variant of binding function Closes #24 --- build.gradle.kts | 1 + .../result/coroutines/SuspendableBinding.kt | 26 ++++ .../michaelbull/result/RunBlockingTest.kt | 9 ++ .../coroutines/SuspendableBindingTest.kt | 135 ++++++++++++++++++ .../michaelbull/result/RunBlockingTest.kt | 6 + 5 files changed, 177 insertions(+) create mode 100644 src/commonMain/kotlin/com/github/michaelbull/result/coroutines/SuspendableBinding.kt create mode 100644 src/commonTest/kotlin/com/github/michaelbull/result/RunBlockingTest.kt create mode 100644 src/commonTest/kotlin/com/github/michaelbull/result/coroutines/SuspendableBindingTest.kt create mode 100644 src/jvmTest/kotlin/com/github/michaelbull/result/RunBlockingTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 86d87b9..af8a49b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -91,6 +91,7 @@ kotlin { dependencies { implementation(kotlin("test-common")) implementation(kotlin("test-annotations-common")) + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-native:1.3.7") } } diff --git a/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/SuspendableBinding.kt b/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/SuspendableBinding.kt new file mode 100644 index 0000000..324337d --- /dev/null +++ b/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/SuspendableBinding.kt @@ -0,0 +1,26 @@ +package com.github.michaelbull.result.coroutines + +import com.github.michaelbull.result.BindException +import com.github.michaelbull.result.Ok +import com.github.michaelbull.result.Result +import com.github.michaelbull.result.ResultBinding +import com.github.michaelbull.result.ResultBindingImpl +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * Suspending variant of [binding][com.github.michaelbull.result.binding]. + */ +suspend inline fun binding(crossinline block: suspend ResultBinding.() -> V): Result { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + + val receiver = ResultBindingImpl() + + return try { + with(receiver) { Ok(block()) } + } catch (ex: BindException) { + receiver.error + } +} diff --git a/src/commonTest/kotlin/com/github/michaelbull/result/RunBlockingTest.kt b/src/commonTest/kotlin/com/github/michaelbull/result/RunBlockingTest.kt new file mode 100644 index 0000000..0fe64d6 --- /dev/null +++ b/src/commonTest/kotlin/com/github/michaelbull/result/RunBlockingTest.kt @@ -0,0 +1,9 @@ +package com.github.michaelbull.result + +import kotlinx.coroutines.CoroutineScope + +/** + * Workaround to use suspending functions in unit tests for multiplatform/native projects. + * Solution was found here: https://github.com/Kotlin/kotlinx.coroutines/issues/885#issuecomment-446586161 + */ +expect fun runBlockingTest(block: suspend (scope : CoroutineScope) -> Unit) diff --git a/src/commonTest/kotlin/com/github/michaelbull/result/coroutines/SuspendableBindingTest.kt b/src/commonTest/kotlin/com/github/michaelbull/result/coroutines/SuspendableBindingTest.kt new file mode 100644 index 0000000..fe47d18 --- /dev/null +++ b/src/commonTest/kotlin/com/github/michaelbull/result/coroutines/SuspendableBindingTest.kt @@ -0,0 +1,135 @@ +package com.github.michaelbull.result.coroutines + +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 kotlinx.coroutines.delay +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class SuspendableBindingTest { + + private object BindingError + + @Test + fun returnsOkIfAllBindsSuccessful() { + suspend fun provideX(): Result { + delay(1) + return Ok(1) + } + + suspend fun provideY(): Result { + delay(1) + return Ok(2) + } + + runBlockingTest { + val result = binding { + val x = provideX().bind() + val y = provideY().bind() + x + y + } + + assertTrue(result is Ok) + assertEquals( + expected = 3, + actual = result.value + ) + } + } + + @Test + fun returnsOkIfAllBindsOfDifferentTypeAreSuccessful() { + suspend fun provideX(): Result { + delay(1) + return Ok("1") + } + + suspend fun provideY(x: Int): Result { + delay(1) + return Ok(x + 2) + } + + runBlockingTest { + val result = binding { + val x = provideX().bind() + val y = provideY(x.toInt()).bind() + y + } + + assertTrue(result is Ok) + assertEquals( + expected = 3, + actual = result.value + ) + } + } + + @Test + fun returnsFirstErrIfBindingFailed() { + suspend fun provideX(): Result { + delay(1) + return Ok(1) + } + + suspend fun provideY(): Result { + delay(1) + return Err(BindingError) + } + + suspend fun provideZ(): Result { + delay(1) + return Ok(2) + } + + runBlockingTest { + val result = binding { + val x = provideX().bind() + val y = provideY().bind() + val z = provideZ().bind() + x + y + z + } + + assertTrue(result is Err) + assertEquals( + expected = BindingError, + actual = result.error + ) + } + } + + @Test + fun returnsFirstErrIfBindingsOfDifferentTypesFailed() { + suspend fun provideX(): Result { + delay(1) + return Ok(1) + } + + suspend fun provideY(): Result { + delay(1) + return Err(BindingError) + } + + suspend fun provideZ(): Result { + delay(1) + return Ok(2) + } + + runBlockingTest { + val result = binding { + val x = provideX().bind() + val y = provideY().bind() + val z = provideZ().bind() + x + y.toInt() + z + } + + assertTrue(result is Err) + assertEquals( + expected = BindingError, + actual = result.error + ) + } + } +} diff --git a/src/jvmTest/kotlin/com/github/michaelbull/result/RunBlockingTest.kt b/src/jvmTest/kotlin/com/github/michaelbull/result/RunBlockingTest.kt new file mode 100644 index 0000000..4fa0512 --- /dev/null +++ b/src/jvmTest/kotlin/com/github/michaelbull/result/RunBlockingTest.kt @@ -0,0 +1,6 @@ +package com.github.michaelbull.result + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.runBlocking + +actual fun runBlockingTest(block: suspend (scope : CoroutineScope) -> Unit) = runBlocking { block(this) }