diff --git a/build.gradle.kts b/build.gradle.kts index 64b3afd..9239bc2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -51,6 +51,7 @@ kotlin { 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:kotest-framework-datatest:5.6.1") implementation("io.kotest.extensions:kotest-assertions-arrow:1.3.3") } } @@ -58,7 +59,12 @@ kotlin { val jvmTest by getting { dependencies { implementation("io.kotest:kotest-runner-junit5-jvm:5.6.1") + implementation("io.kotest:kotest-runner-junit5:5.6.1") } } } } + +tasks.withType().configureEach { + useJUnitPlatform() +} diff --git a/src/commonMain/kotlin/arrow.exact/Exact.kt b/src/commonMain/kotlin/arrow.exact/Exact.kt index a1dc90d..875777a 100644 --- a/src/commonMain/kotlin/arrow.exact/Exact.kt +++ b/src/commonMain/kotlin/arrow.exact/Exact.kt @@ -1,23 +1,28 @@ package arrow.exact -import arrow.core.* +import arrow.core.Either -interface Exact { - fun from(value: A): Either +interface Exact : ExactEither - fun fromOrNull(value: A): B? { - return from(value).getOrNull() - } +data class ExactError(val message: String) + +interface ExactEither { + + fun from(value: A): Either + + // TODO: This doesn't work for some weird reason :/ + // fun Raise.fromOrRaise(value: A): R = from(value).bind() - fun fromOrThrow(value: A): B { - return when (val result = from(value)) { - is Either.Left -> throw ExactException(result.value.message) - is Either.Right -> result.value - } + fun fromOrNull(value: A): R? = from(value).getOrNull() + + fun fromOrThrow(value: A): R = when (val result = from(value)) { + is Either.Left -> throw ExactException(result.value) + is Either.Right -> result.value } -} -open class ExactError(val message: String) + // TODO: What are your thoughts about this? + operator fun invoke(value: A): R = fromOrThrow(value) +} -class ExactException(message: String) : IllegalArgumentException(message) +class ExactException(error: Any) : IllegalArgumentException("ArrowExact error: $error") diff --git a/src/commonMain/kotlin/arrow.exact/ExactDsl.kt b/src/commonMain/kotlin/arrow.exact/ExactDsl.kt index f1a5fbe..0130e1e 100644 --- a/src/commonMain/kotlin/arrow.exact/ExactDsl.kt +++ b/src/commonMain/kotlin/arrow.exact/ExactDsl.kt @@ -1,30 +1,22 @@ package arrow.exact -import arrow.core.* +import arrow.core.Either +import arrow.core.raise.Raise +import arrow.core.raise.either -internal class AndExact( - private val exact1: Exact, - private val exact2: Exact -) : Exact { +@DslMarker +annotation class ExactDsl - override fun from(value: A): Either { - return exact1.from(value) - .flatMap { exact2.from(it) } - } +@ExactDsl +fun exact( + construct: Raise.(A) -> R +): Exact = object : Exact { + override fun from(value: A): Either = either { construct(value) } } -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) +@ExactDsl +fun exactEither( + construct: Raise.(A) -> R +): ExactEither = object : ExactEither { + override fun from(value: A): Either = either { construct(value) } } diff --git a/src/commonMain/kotlin/arrow.exact/demo/Demo.kt b/src/commonMain/kotlin/arrow.exact/demo/Demo.kt new file mode 100644 index 0000000..c1c238a --- /dev/null +++ b/src/commonMain/kotlin/arrow.exact/demo/Demo.kt @@ -0,0 +1,77 @@ +package arrow.exact.demo + +import arrow.core.Either +import arrow.core.flatMap +import arrow.core.raise.either +import arrow.core.raise.ensure +import arrow.core.right +import arrow.exact.* +import kotlin.jvm.JvmInline +import kotlin.random.Random + +// TODO: We need a lint check telling people to make their constructors private +@JvmInline +value class NotBlankString private constructor( + val value: String +) { + companion object : Exact by exact({ + ensure(it.isNotBlank()) { ExactError("Cannot be blank.") } + NotBlankString(it) + }) +} + +// TODO: We need a lint check telling people to make their constructors private +@JvmInline +value class NotBlankTrimmedString private constructor( + val value: String +) { + companion object : Exact by exact({ raw -> + val notBlank = NotBlankString.from(raw).bind() + NotBlankTrimmedString(notBlank.value.trim()) + }) +} + +sealed interface UsernameError { + object Invalid : UsernameError + data class Offensive(val username: String) : UsernameError +} + +@JvmInline +value class Username private constructor( + val value: String +) { + companion object : ExactEither by exactEither({ rawUsername -> + val username = NotBlankTrimmedString.from(rawUsername) // compose Exact + .mapLeft { UsernameError.Invalid }.bind().value + ensure(username.length < 100) { UsernameError.Invalid } + ensure(username !in listOf("offensive")) { UsernameError.Offensive(username) } + Username(username) + }) +} + +@JvmInline +value class PositiveInt private constructor( + val value: Int +) { + companion object : Exact by exact({ + ensure(it > 0) { ExactError("Must be positive.") } + PositiveInt(it) + }) +} + +fun demo(): Either = either { + val hello = NotBlankTrimmedString("Hello") + val world = NotBlankTrimmedString("World") + + val helloWorld = NotBlankTrimmedString.from(hello.value + " " + world.value) + .mapLeft { it.message }.bind() + val username = Username.from("user1") + .mapLeft { it.toString() }.bind() + + + val x = PositiveInt(3) + val y = PositiveInt.from(Random.nextInt()) + val z = y.flatMap { y1 -> + PositiveInt(x.value + y1.value).right() + } +} diff --git a/src/commonTest/kotlin/arrow/exact/ExactSpec.kt b/src/commonTest/kotlin/arrow/exact/ExactSpec.kt deleted file mode 100644 index 1bbe95e..0000000 --- a/src/commonTest/kotlin/arrow/exact/ExactSpec.kt +++ /dev/null @@ -1,37 +0,0 @@ -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 deleted file mode 100644 index 330d69f..0000000 --- a/src/commonTest/kotlin/arrow/exact/Strings.kt +++ /dev/null @@ -1,21 +0,0 @@ -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 -}