diff --git a/kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/RunSuspendCatching.kt b/kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/RunSuspendCatching.kt new file mode 100644 index 0000000..36174f0 --- /dev/null +++ b/kotlin-result-coroutines/src/commonMain/kotlin/com/github/michaelbull/result/coroutines/RunSuspendCatching.kt @@ -0,0 +1,42 @@ +package com.github.michaelbull.result.coroutines + +import com.github.michaelbull.result.Result +import com.github.michaelbull.result.runCatching +import com.github.michaelbull.result.throwIf +import kotlinx.coroutines.CancellationException +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * Calls the specified function [block] and returns its encapsulated result if invocation was + * successful, catching any [Throwable] exception that was thrown from the [block] function + * execution and encapsulating it as a failure. If the encapsulated failure is a + * [CancellationException], the exception is thrown to indicate _normal_ cancellation of a + * coroutine. + */ +public inline fun runSuspendCatching(block: () -> V): Result { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + + return runCatching(block).throwIf { + it is CancellationException + } +} + +/** + * Calls the specified function [block] with [this] value as its receiver and returns its + * encapsulated result if invocation was successful, catching any [Throwable] exception that was + * thrown from the [block] function execution and encapsulating it as a failure. If the + * encapsulated failure is a [CancellationException], the exception is thrown to indicate _normal_ + * cancellation of a coroutine. + */ +public inline infix fun T.runSuspendCatching(block: T.() -> V): Result { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + + return runCatching(block).throwIf { + it is CancellationException + } +} diff --git a/kotlin-result-coroutines/src/jvmTest/kotlin/com/github/michaelbull/result/coroutines/RunSuspendCatchingTest.kt b/kotlin-result-coroutines/src/jvmTest/kotlin/com/github/michaelbull/result/coroutines/RunSuspendCatchingTest.kt new file mode 100644 index 0000000..2310930 --- /dev/null +++ b/kotlin-result-coroutines/src/jvmTest/kotlin/com/github/michaelbull/result/coroutines/RunSuspendCatchingTest.kt @@ -0,0 +1,65 @@ +package com.github.michaelbull.result.coroutines + +import com.github.michaelbull.result.Err +import com.github.michaelbull.result.Ok +import com.github.michaelbull.result.onSuccess +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runBlockingTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +@ExperimentalCoroutinesApi +class RunSuspendCatchingTest { + + @Test + fun propagatesCoroutineCancellation() = runBlockingTest { + var value: String? = null + + launch(CoroutineName("outer scope")) { + launch(CoroutineName("inner scope")) { + val result = runSuspendCatching { + delay(4_000) + "value" + } + + // The coroutine should be cancelled before reaching here + result.onSuccess { value = it } + } + + advanceTimeBy(2_000) + + // Cancel outer scope, which should cancel inner scope + cancel() + } + + assertNull(value) + } + + @Test + fun returnsOkIfInvocationSuccessful() = runBlockingTest { + val block = { "example" } + val result = runSuspendCatching(block) + + assertEquals( + expected = Ok("example"), + actual = result + ) + } + + @Test + fun returnsErrIfInvocationFailsWithAnythingOtherThanCancellationException() = runBlockingTest { + val exception = IllegalArgumentException("throw me") + val block = { throw exception } + val result = runSuspendCatching(block) + + assertEquals( + expected = Err(exception), + actual = result + ) + } +} diff --git a/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Factory.kt b/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Factory.kt index 53a5ed8..216f653 100644 --- a/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Factory.kt +++ b/kotlin-result/src/commonMain/kotlin/com/github/michaelbull/result/Factory.kt @@ -4,9 +4,9 @@ import kotlin.contracts.InvocationKind import kotlin.contracts.contract /** - * Calls the specified function [block] and returns its encapsulated result if - * invocation was successful, catching and encapsulating any thrown exception - * as a failure. + * Calls the specified function [block] and returns its encapsulated result if invocation was + * successful, catching any [Throwable] exception that was thrown from the [block] function + * execution and encapsulating it as a failure. */ public inline fun runCatching(block: () -> V): Result { contract { @@ -21,9 +21,9 @@ public inline fun runCatching(block: () -> V): Result { } /** - * Calls the specified function [block] with [this] value as its receiver and - * returns its encapsulated result if invocation was successful, catching and - * encapsulating any thrown exception as a failure. + * Calls the specified function [block] with [this] value as its receiver and returns its + * encapsulated result if invocation was successful, catching any [Throwable] exception that was + * thrown from the [block] function execution and encapsulating it as a failure. */ public inline infix fun T.runCatching(block: T.() -> V): Result { contract {