diff --git a/build.gradle.kts b/build.gradle.kts index 3f0aa43..64b3afd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - kotlin("multiplatform") version "1.6.21" apply true - id("io.kotest.multiplatform") version "5.3.0" apply true + kotlin("multiplatform") version "1.8.21" apply true + id("io.kotest.multiplatform") version "5.6.0" apply true } group "org.example" @@ -25,7 +25,6 @@ kotlin { mingwX64() - iosArm32() iosArm64() iosSimulatorArm64() iosX64() @@ -38,33 +37,27 @@ kotlin { watchosArm64() watchosSimulatorArm64() watchosX64() - watchosX86() sourceSets { commonMain { dependencies { implementation(kotlin("stdlib-common")) - implementation("io.arrow-kt:arrow-core:1.1.3-alpha.39") - implementation("io.arrow-kt:arrow-optics:1.1.3-alpha.39") - implementation("io.arrow-kt:arrow-fx-coroutines:1.1.3-alpha.39") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1") + implementation("io.arrow-kt:arrow-core:1.2.0-RC") } } commonTest { dependencies { - implementation("io.kotest:kotest-property:5.3.0") - implementation("io.kotest:kotest-framework-engine:5.3.0") - implementation("io.kotest:kotest-assertions-core:5.3.0") - implementation("io.kotest.extensions:kotest-assertions-arrow:1.2.5") - implementation("io.kotest.extensions:kotest-property-arrow:1.2.5") // optional - implementation("io.kotest.extensions:kotest-property-arrow-optics:1.2.5") // optional + implementation("io.kotest:kotest-property:5.6.1") + implementation("io.kotest:kotest-framework-engine:5.6.1") + implementation("io.kotest:kotest-assertions-core:5.6.1") + implementation("io.kotest.extensions:kotest-assertions-arrow:1.3.3") } } val jvmTest by getting { dependencies { - implementation("io.kotest:kotest-runner-junit5-jvm:5.3.0") + implementation("io.kotest:kotest-runner-junit5-jvm:5.6.1") } } } diff --git a/src/commonMain/kotlin/arrow.exact/Exact.kt b/src/commonMain/kotlin/arrow.exact/Exact.kt new file mode 100644 index 0000000..a1dc90d --- /dev/null +++ b/src/commonMain/kotlin/arrow.exact/Exact.kt @@ -0,0 +1,23 @@ +package arrow.exact + +import arrow.core.* + +interface Exact { + + fun from(value: A): Either + + fun fromOrNull(value: A): B? { + return from(value).getOrNull() + } + + fun fromOrThrow(value: A): B { + return when (val result = from(value)) { + is Either.Left -> throw ExactException(result.value.message) + is Either.Right -> result.value + } + } +} + +open class ExactError(val message: String) + +class ExactException(message: String) : IllegalArgumentException(message) diff --git a/src/commonMain/kotlin/arrow.exact/ExactDsl.kt b/src/commonMain/kotlin/arrow.exact/ExactDsl.kt new file mode 100644 index 0000000..f1a5fbe --- /dev/null +++ b/src/commonMain/kotlin/arrow.exact/ExactDsl.kt @@ -0,0 +1,30 @@ +package arrow.exact + +import arrow.core.* + +internal class AndExact( + private val exact1: Exact, + private val exact2: Exact +) : Exact { + + override fun from(value: A): Either { + return exact1.from(value) + .flatMap { exact2.from(it) } + } +} + +fun exact(predicate: Predicate, constructor: (A) -> B): Exact { + return object : Exact { + override fun from(value: A): Either { + return if (predicate.invoke(value)) { + constructor.invoke(value).right() + } else { + ExactError("Value ($value) doesn't match the predicate").left() + } + } + } +} + +infix fun Exact.and(other: Exact): Exact { + return AndExact(this, other) +} diff --git a/src/commonMain/kotlin/example.kt b/src/commonMain/kotlin/example.kt deleted file mode 100644 index 84c2f38..0000000 --- a/src/commonMain/kotlin/example.kt +++ /dev/null @@ -1,37 +0,0 @@ -@file:Suppress("RedundantSuspendModifier") - -import arrow.core.Either -import arrow.core.Either.Left -import arrow.core.Either.Right -import arrow.core.continuations.either - -object Lettuce -object Knife -object Salad - -sealed class CookingException { - object LettuceIsRotten : CookingException() - object KnifeNeedsSharpening : CookingException() - data class InsufficientAmount(val quantityInGrams: Int) : CookingException() -} - -typealias NastyLettuce = CookingException.LettuceIsRotten -typealias KnifeIsDull = CookingException.KnifeNeedsSharpening -typealias InsufficientAmountOfLettuce = CookingException.InsufficientAmount - -fun takeFoodFromRefrigerator(): Either = - Right(Lettuce) - -suspend fun getKnife(): Either = - Right(Knife) - -fun prepare(tool: Knife, ingredient: Lettuce): Either = - Left(InsufficientAmountOfLettuce(5)) - -suspend fun prepareLunch(): Either = - either { - val lettuce = takeFoodFromRefrigerator().bind() - val knife = getKnife().bind() - val lunch = prepare(knife, lettuce).bind() - lunch - } diff --git a/src/commonTest/kotlin/ExampleSpec.kt b/src/commonTest/kotlin/ExampleSpec.kt deleted file mode 100644 index 34ab53d..0000000 --- a/src/commonTest/kotlin/ExampleSpec.kt +++ /dev/null @@ -1,73 +0,0 @@ -import arrow.core.Either -import arrow.optics.Traversal -import arrow.typeclasses.Monoid -import io.kotest.assertions.arrow.core.shouldBeRight -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe -import io.kotest.matchers.types.shouldBeTypeOf -import io.kotest.property.Arb -import io.kotest.property.arbitrary.int -import io.kotest.property.arbitrary.list -import io.kotest.property.arbitrary.map -import io.kotest.property.arbitrary.positiveInt -import io.kotest.property.arbitrary.string -import io.kotest.property.arrow.core.MonoidLaws -import io.kotest.property.arrow.core.either -import io.kotest.property.arrow.core.functionAToB -import io.kotest.property.arrow.laws.testLaws -import io.kotest.property.arrow.optics.TraversalLaws -import kotlin.jvm.JvmInline - -class ExampleSpec : StringSpec({ - "true shouldBe true" { - true shouldBe true - } - - "exception should fail" { - // throw RuntimeException("Boom2!") - } - - "kotest arrow extension use-cases" { - // smart-cast abilities for arrow types - Either.Right("HI").shouldBeRight() shouldBe "HI" - } - - // utilise builtin or costume Laws with Generators to verify behavior - testLaws( - MonoidLaws.laws(Monoid.list(), Arb.list(Arb.string())), - MonoidLaws.laws(Monoid.numbers(), Arb.numbers()) - ) - - // optics Laws from arrow - testLaws( - TraversalLaws.laws( - traversal = Traversal.either(), - aGen = Arb.either(Arb.string(), Arb.int()), - bGen = Arb.int(), - funcGen = Arb.functionAToB(Arb.int()), - ) - ) -}) - -fun Arb.Companion.numbers(): Arb = - Arb.positiveInt().map { it.toNumber() } - -fun Int.toNumber(): Numbers = - if (this <= 0) Zero else Suc(minus(1).toNumber()) - -// natural numbers form a monoid -fun Monoid.Companion.numbers(): Monoid = - object : Monoid { - override fun empty(): Numbers = - Zero - - override fun Numbers.combine(b: Numbers): Numbers = - Suc(b) - } - -// natural numbers -sealed interface Numbers -object Zero : Numbers - -@JvmInline -value class Suc(val value: Numbers) : Numbers diff --git a/src/commonTest/kotlin/arrow/exact/ExactSpec.kt b/src/commonTest/kotlin/arrow/exact/ExactSpec.kt new file mode 100644 index 0000000..1bbe95e --- /dev/null +++ b/src/commonTest/kotlin/arrow/exact/ExactSpec.kt @@ -0,0 +1,37 @@ +package arrow.exact + +import io.kotest.assertions.arrow.core.shouldBeRight +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe + +class ExactSpec : StringSpec({ + "creates NotBlankTrimmedString" { + val notBlank = NotBlankTrimmedString.fromOrThrow(" test ") + notBlank.str shouldBe "test" + } + + "throws exception on failed check" { + shouldThrow { + NotBlankTrimmedString.fromOrThrow(" ") + } + } + + "returns not null" { + NotBlankTrimmedString.fromOrNull("test") shouldNotBe null + } + + "returns null" { + NotBlankTrimmedString.fromOrNull(" ") shouldBe null + } + + "returns right" { + val either = NotBlankTrimmedString.from(" test ") + either.map { it.str } shouldBeRight "test" + } + + "returns left" { + NotBlankTrimmedString.from(" ").isLeft() shouldBe true + } +}) diff --git a/src/commonTest/kotlin/arrow/exact/Strings.kt b/src/commonTest/kotlin/arrow/exact/Strings.kt new file mode 100644 index 0000000..330d69f --- /dev/null +++ b/src/commonTest/kotlin/arrow/exact/Strings.kt @@ -0,0 +1,21 @@ +package arrow.exact + +import arrow.core.Either +import arrow.core.right + +class NotBlankString private constructor(val str: String) { + companion object : Exact by exact(String::isNotBlank, ::NotBlankString) +} + +class NotBlankTrimmedString private constructor(val str: String) { + + private object TrimmedString : Exact { + + override fun from(value: NotBlankString): Either { + val trimmed = value.str.trim() + return NotBlankTrimmedString(trimmed).right() + } + } + + companion object : Exact by NotBlankString and TrimmedString +}