From 312fa8a6031a64cbc388d7b45cd6d80b64676563 Mon Sep 17 00:00:00 2001 From: Chris Myers Date: Tue, 10 Sep 2024 10:43:40 +1000 Subject: [PATCH 1/2] Adds in conversion to and from Result> and OutcomeOf --- lib/api/lib.api | 2 ++ .../main/kotlin/app/cash/quiver/Outcome.kt | 4 +++ .../app/cash/quiver/extensions/Result.kt | 10 +++++++ .../kotlin/app/cash/quiver/OutcomeTest.kt | 27 ++++++++++++++++--- .../app/cash/quiver/extensions/ResultTest.kt | 7 +++++ testing-lib/api/testing-lib.api | 2 ++ .../kotlin/app/cash/quiver/arb/Arbitrary.kt | 9 +++++++ 7 files changed, 58 insertions(+), 3 deletions(-) diff --git a/lib/api/lib.api b/lib/api/lib.api index 043a89f..a54cecf 100644 --- a/lib/api/lib.api +++ b/lib/api/lib.api @@ -36,6 +36,7 @@ public final class app/cash/quiver/OutcomeKt { public static final fun asEither (Lapp/cash/quiver/Outcome;Lkotlin/jvm/functions/Function0;)Larrow/core/Either; public static final fun asOption (Lapp/cash/quiver/Outcome;)Larrow/core/Option; public static final fun asOutcome (Larrow/core/Either;)Lapp/cash/quiver/Outcome; + public static final fun asResult (Lapp/cash/quiver/Outcome;)Ljava/lang/Object; public static final fun failure (Ljava/lang/Object;)Lapp/cash/quiver/Outcome; public static final fun filter (Lapp/cash/quiver/Outcome;Lkotlin/jvm/functions/Function1;)Lapp/cash/quiver/Outcome; public static final fun flatMap (Lapp/cash/quiver/Outcome;Lkotlin/jvm/functions/Function1;)Lapp/cash/quiver/Outcome; @@ -288,6 +289,7 @@ public final class app/cash/quiver/extensions/ResultKt { public static final fun tap (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public static final fun toEither (Ljava/lang/Object;)Larrow/core/Either; public static final fun toOutcome (Ljava/lang/Object;)Lapp/cash/quiver/Outcome; + public static final fun toOutcomeOf (Ljava/lang/Object;)Lapp/cash/quiver/Outcome; public static final fun toResult (Larrow/core/Option;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; public static final fun toResult (Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; public static final fun tryCatch (Lkotlin/Result$Companion;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; diff --git a/lib/src/main/kotlin/app/cash/quiver/Outcome.kt b/lib/src/main/kotlin/app/cash/quiver/Outcome.kt index 5d3d551..4f04f18 100644 --- a/lib/src/main/kotlin/app/cash/quiver/Outcome.kt +++ b/lib/src/main/kotlin/app/cash/quiver/Outcome.kt @@ -2,6 +2,7 @@ package app.cash.quiver +import app.cash.quiver.extensions.OutcomeOf import arrow.core.Either import arrow.core.Either.Left import arrow.core.Either.Right @@ -17,6 +18,7 @@ import arrow.core.right import arrow.core.some import arrow.core.valid import app.cash.quiver.extensions.orThrow +import app.cash.quiver.extensions.toResult import app.cash.quiver.raise.OutcomeRaise import app.cash.quiver.raise.outcome import arrow.core.raise.catch @@ -239,6 +241,8 @@ fun Outcome.asOption(): Option = inner.getOrElse { None } inline fun Outcome.asEither(onAbsent: () -> E): Either = inner.flatMap { it.map(::Right).getOrElse { onAbsent().left() } } +fun OutcomeOf.asResult(): Result> = inner.toResult() + inline fun Outcome.foldOption(onAbsent: () -> B, onPresent: (A) -> B): Either = inner.map { it.fold(onAbsent, onPresent) } diff --git a/lib/src/main/kotlin/app/cash/quiver/extensions/Result.kt b/lib/src/main/kotlin/app/cash/quiver/extensions/Result.kt index bd0bc5b..7c76968 100644 --- a/lib/src/main/kotlin/app/cash/quiver/extensions/Result.kt +++ b/lib/src/main/kotlin/app/cash/quiver/extensions/Result.kt @@ -1,6 +1,10 @@ package app.cash.quiver.extensions +import app.cash.quiver.Failure +import app.cash.quiver.Outcome +import app.cash.quiver.Present import app.cash.quiver.asOutcome +import app.cash.quiver.toOutcome import arrow.core.Either import arrow.core.Option import arrow.core.flatMap @@ -19,6 +23,12 @@ fun Result.toEither(): ErrorOr = this.map { Either.Right(it) }.getOrEl */ fun Result.toOutcome(): OutcomeOf = this.map { Either.Right(it) }.getOrElse { Either.Left(it) }.asOutcome() +/** + * Transforms a `Result>` to an `OutcomeOf` + */ +fun Result>.toOutcomeOf(): OutcomeOf = this.toEither().toOutcome() + + /** * Make anything a Success. */ diff --git a/lib/src/test/kotlin/app/cash/quiver/OutcomeTest.kt b/lib/src/test/kotlin/app/cash/quiver/OutcomeTest.kt index 74c32a9..f66f0c1 100644 --- a/lib/src/test/kotlin/app/cash/quiver/OutcomeTest.kt +++ b/lib/src/test/kotlin/app/cash/quiver/OutcomeTest.kt @@ -3,6 +3,8 @@ package app.cash.quiver import app.cash.quiver.arb.outcome +import app.cash.quiver.arb.outcomeOf +import app.cash.quiver.arb.result import app.cash.quiver.matchers.shouldBeAbsent import app.cash.quiver.matchers.shouldBeFailure import app.cash.quiver.matchers.shouldBePresent @@ -17,7 +19,7 @@ import arrow.core.right import arrow.core.some import arrow.core.valid import app.cash.quiver.continuations.outcome -import app.cash.quiver.extensions.success +import app.cash.quiver.extensions.toOutcomeOf import io.kotest.assertions.arrow.core.shouldBeInvalid import io.kotest.assertions.arrow.core.shouldBeLeft import io.kotest.assertions.arrow.core.shouldBeNone @@ -28,6 +30,7 @@ import io.kotest.assertions.fail import io.kotest.assertions.throwables.shouldThrow import io.kotest.common.runBlocking import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.result.shouldBeFailure import io.kotest.matchers.shouldBe import io.kotest.property.Arb import io.kotest.property.arbitrary.int @@ -474,9 +477,27 @@ class OutcomeTest : StringSpec({ } "recover can recover from Failure" { checkAll(Arb.either(Arb.long(), Arb.int())) { either -> - val x: Outcome = "failure".failure() - x.recover { either.bind() } + val failed: Outcome = "failure".failure() + failed.recover { either.bind() } .asEither { fail("Cannot be absent") }.shouldBe(either) } } + + "can transform from result to OutcomeOf and back again" { + checkAll(Arb.result(Throwable("boom"), Arb.option(Arb.int()))) { input -> + input.toOutcomeOf().asResult() shouldBe input + } + } + + "can transform from OutcomeOf to result and back again" { + checkAll(Arb.outcomeOf(Throwable("boom"), Arb.int())) { outcome -> + outcome.asResult().toOutcomeOf() shouldBe outcome + } + } + + "Converting to Result" { + Absent.asResult() shouldBe Result.success(None) + 1.present().asResult() shouldBe Result.success(Some(1)) + Throwable("sad").failure().asResult().shouldBeFailure().message shouldBe "sad" + } }) diff --git a/lib/src/test/kotlin/app/cash/quiver/extensions/ResultTest.kt b/lib/src/test/kotlin/app/cash/quiver/extensions/ResultTest.kt index 9d6f39f..30782a8 100644 --- a/lib/src/test/kotlin/app/cash/quiver/extensions/ResultTest.kt +++ b/lib/src/test/kotlin/app/cash/quiver/extensions/ResultTest.kt @@ -1,5 +1,6 @@ package app.cash.quiver.extensions +import app.cash.quiver.matchers.shouldBeAbsent import app.cash.quiver.matchers.shouldBeFailure import app.cash.quiver.matchers.shouldBePresent import arrow.core.None @@ -138,4 +139,10 @@ class ResultTest : StringSpec({ .shouldBeFailure().message shouldBe "sad panda" } + "Converting to Outcome" { + Throwable("sad panda").failure().toOutcome().shouldBeFailure() + Result.success(Some("yay")).toOutcomeOf().shouldBePresent().shouldBe("yay") + Result.success(None).toOutcomeOf().shouldBeAbsent() + } + }) diff --git a/testing-lib/api/testing-lib.api b/testing-lib/api/testing-lib.api index 1e5b948..de8f855 100644 --- a/testing-lib/api/testing-lib.api +++ b/testing-lib/api/testing-lib.api @@ -1,6 +1,8 @@ public final class app/cash/quiver/arb/ArbitraryKt { public static final fun ior (Lio/kotest/property/Arb$Companion;Lio/kotest/property/Arb;Lio/kotest/property/Arb;)Lio/kotest/property/Arb; public static final fun outcome (Lio/kotest/property/Arb$Companion;Lio/kotest/property/Arb;Lio/kotest/property/Arb;)Lio/kotest/property/Arb; + public static final fun outcomeOf (Lio/kotest/property/Arb$Companion;Ljava/lang/Throwable;Lio/kotest/property/Arb;)Lio/kotest/property/Arb; + public static final fun result (Lio/kotest/property/Arb$Companion;Ljava/lang/Throwable;Lio/kotest/property/Arb;)Lio/kotest/property/Arb; } public final class app/cash/quiver/matchers/MatchersKt { diff --git a/testing-lib/src/main/kotlin/app/cash/quiver/arb/Arbitrary.kt b/testing-lib/src/main/kotlin/app/cash/quiver/arb/Arbitrary.kt index 9cb4f5a..18d969a 100644 --- a/testing-lib/src/main/kotlin/app/cash/quiver/arb/Arbitrary.kt +++ b/testing-lib/src/main/kotlin/app/cash/quiver/arb/Arbitrary.kt @@ -1,6 +1,8 @@ package app.cash.quiver.arb import app.cash.quiver.Outcome +import app.cash.quiver.extensions.OutcomeOf +import app.cash.quiver.extensions.success import app.cash.quiver.toOutcome import arrow.core.Ior import arrow.core.leftIor @@ -9,12 +11,19 @@ import io.kotest.property.Arb import io.kotest.property.arbitrary.bind import io.kotest.property.arbitrary.choice import io.kotest.property.arbitrary.map +import io.kotest.property.arbitrary.of import io.kotest.property.arrow.core.either import io.kotest.property.arrow.core.option fun Arb.Companion.outcome(error: Arb, value: Arb): Arb> = Arb.either(error, Arb.option(value)).map { it.toOutcome() } +fun Arb.Companion.outcomeOf(error: Throwable, value: Arb): Arb> = + Arb.either(Arb.of(error), Arb.option(value)).map { it.toOutcome() } + +fun Arb.Companion.result(error: Throwable, value: Arb): Arb> = + Arb.option(value).map { option -> option.fold({ Result.failure(error) }, { it.success() }) } + fun Arb.Companion.ior(error: Arb, value: Arb): Arb> = Arb.choice( error.map { it.leftIor() }, From d301bd206e5b5534a85ed9708a36857e7337e0dd Mon Sep 17 00:00:00 2001 From: Chris Myers Date: Tue, 10 Sep 2024 10:48:08 +1000 Subject: [PATCH 2/2] Adds documentation --- lib/src/main/kotlin/app/cash/quiver/Outcome.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/main/kotlin/app/cash/quiver/Outcome.kt b/lib/src/main/kotlin/app/cash/quiver/Outcome.kt index 4f04f18..7103939 100644 --- a/lib/src/main/kotlin/app/cash/quiver/Outcome.kt +++ b/lib/src/main/kotlin/app/cash/quiver/Outcome.kt @@ -241,6 +241,10 @@ fun Outcome.asOption(): Option = inner.getOrElse { None } inline fun Outcome.asEither(onAbsent: () -> E): Either = inner.flatMap { it.map(::Right).getOrElse { onAbsent().left() } } +/** + * Converts an OutcomeOf to a Result>. This reflects the inner structure of the + * Outcome. + */ fun OutcomeOf.asResult(): Result> = inner.toResult() inline fun Outcome.foldOption(onAbsent: () -> B, onPresent: (A) -> B): Either =