Skip to content

Commit

Permalink
Adds in conversion to and from Result<Option<A>> and OutcomeOf<A> (#104)
Browse files Browse the repository at this point in the history
* Adds in conversion to and from Result<Option<A>> and OutcomeOf<A>

* Adds documentation
  • Loading branch information
cwmyers authored Sep 10, 2024
1 parent 0819cf8 commit 0df954c
Show file tree
Hide file tree
Showing 7 changed files with 62 additions and 3 deletions.
2 changes: 2 additions & 0 deletions lib/api/lib.api
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions lib/src/main/kotlin/app/cash/quiver/Outcome.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -239,6 +241,12 @@ fun <E, A> Outcome<E, A>.asOption(): Option<A> = inner.getOrElse { None }
inline fun <E, A> Outcome<E, A>.asEither(onAbsent: () -> E): Either<E, A> =
inner.flatMap { it.map(::Right).getOrElse { onAbsent().left() } }

/**
* Converts an OutcomeOf<A> to a Result<Option<A>>. This reflects the inner structure of the
* Outcome.
*/
fun <A> OutcomeOf<A>.asResult(): Result<Option<A>> = inner.toResult()

inline fun <E, A, B> Outcome<E, A>.foldOption(onAbsent: () -> B, onPresent: (A) -> B): Either<E, B> =
inner.map { it.fold(onAbsent, onPresent) }

Expand Down
10 changes: 10 additions & 0 deletions lib/src/main/kotlin/app/cash/quiver/extensions/Result.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,6 +23,12 @@ fun <T> Result<T>.toEither(): ErrorOr<T> = this.map { Either.Right(it) }.getOrEl
*/
fun <T> Result<T>.toOutcome(): OutcomeOf<T> = this.map { Either.Right(it) }.getOrElse { Either.Left(it) }.asOutcome()

/**
* Transforms a `Result<Option<T>>` to an `OutcomeOf<A>`
*/
fun <T> Result<Option<T>>.toOutcomeOf(): OutcomeOf<T> = this.toEither().toOutcome()


/**
* Make anything a Success.
*/
Expand Down
27 changes: 24 additions & 3 deletions lib/src/test/kotlin/app/cash/quiver/OutcomeTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -474,9 +477,27 @@ class OutcomeTest : StringSpec({
}
"recover can recover from Failure" {
checkAll(Arb.either(Arb.long(), Arb.int())) { either ->
val x: Outcome<String, Int> = "failure".failure()
x.recover { either.bind() }
val failed: Outcome<String, Int> = "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"
}
})
7 changes: 7 additions & 0 deletions lib/src/test/kotlin/app/cash/quiver/extensions/ResultTest.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -138,4 +139,10 @@ class ResultTest : StringSpec({
.shouldBeFailure().message shouldBe "sad panda"
}

"Converting to Outcome" {
Throwable("sad panda").failure<Int>().toOutcome().shouldBeFailure()
Result.success(Some("yay")).toOutcomeOf().shouldBePresent().shouldBe("yay")
Result.success(None).toOutcomeOf().shouldBeAbsent()
}

})
2 changes: 2 additions & 0 deletions testing-lib/api/testing-lib.api
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
9 changes: 9 additions & 0 deletions testing-lib/src/main/kotlin/app/cash/quiver/arb/Arbitrary.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 <E, A> Arb.Companion.outcome(error: Arb<E>, value: Arb<A>): Arb<Outcome<E, A>> =
Arb.either(error, Arb.option(value)).map { it.toOutcome() }

fun <A> Arb.Companion.outcomeOf(error: Throwable, value: Arb<A>): Arb<OutcomeOf<A>> =
Arb.either(Arb.of(error), Arb.option(value)).map { it.toOutcome() }

fun <A> Arb.Companion.result(error: Throwable, value: Arb<A>): Arb<Result<A>> =
Arb.option(value).map { option -> option.fold({ Result.failure(error) }, { it.success() }) }

fun <E, A> Arb.Companion.ior(error: Arb<E>, value: Arb<A>): Arb<Ior<E, A>> =
Arb.choice(
error.map { it.leftIor() },
Expand Down

0 comments on commit 0df954c

Please sign in to comment.