From f9ad9b9c7a79794df4e65d5e7c112ffd21e988f4 Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Fri, 5 May 2023 17:38:16 +0300 Subject: [PATCH 01/12] Draft ExactBuilderDsl --- src/commonMain/kotlin/arrow.exact/Exact.kt | 4 +- .../kotlin/arrow.exact/ExactBuilderDsl.kt | 56 +++++++++++++++++++ src/commonMain/kotlin/arrow.exact/ExactDsl.kt | 15 +++-- 3 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 src/commonMain/kotlin/arrow.exact/ExactBuilderDsl.kt diff --git a/src/commonMain/kotlin/arrow.exact/Exact.kt b/src/commonMain/kotlin/arrow.exact/Exact.kt index a1dc90d..4829d37 100644 --- a/src/commonMain/kotlin/arrow.exact/Exact.kt +++ b/src/commonMain/kotlin/arrow.exact/Exact.kt @@ -1,6 +1,6 @@ package arrow.exact -import arrow.core.* +import arrow.core.Either interface Exact { @@ -18,6 +18,6 @@ interface Exact { } } -open class ExactError(val message: String) +class ExactError(val message: String) class ExactException(message: String) : IllegalArgumentException(message) diff --git a/src/commonMain/kotlin/arrow.exact/ExactBuilderDsl.kt b/src/commonMain/kotlin/arrow.exact/ExactBuilderDsl.kt new file mode 100644 index 0000000..1aa56d7 --- /dev/null +++ b/src/commonMain/kotlin/arrow.exact/ExactBuilderDsl.kt @@ -0,0 +1,56 @@ +package arrow.exact + +import arrow.core.* +import arrow.core.raise.Raise +import arrow.core.raise.recover +import kotlin.jvm.JvmInline + +fun exactBuilder( + block: (ExactBuilder) -> Either +): Exact { + return object : Exact { + override fun from(value: A): Either = + block(ExactBuilderDsl(value.right())) + } +} + +class ExactBuilderDsl( + private val value: Either +) : ExactBuilder { + + override fun mustBe(predicate: Predicate): ExactBuilder = ExactBuilderDsl( + value = value.flatMap { + if (predicate(it)) it.right() else ExactError("Predicate failed").left() + } + ) + + override fun transform(transformation: (A) -> B): ExactBuilder = ExactBuilderDsl( + value = value.map(transformation) + ) + + override fun transformOrRaise( + transformation: Raise.(A) -> B + ): ExactBuilder = ExactBuilderDsl( + value = value.flatMap { + recover({ transformation(it).right() }) { ExactError("Transform or raise failed").left() } + } + ) + + override fun build(constructor: (A) -> Constraint): Either = value.map(constructor) +} + +interface ExactBuilder { + fun mustBe(predicate: Predicate): ExactBuilder + fun transform(transformation: (A) -> B): ExactBuilder + fun transformOrRaise(transformation: Raise.(A) -> B): ExactBuilder + fun build(constructor: (A) -> Constraint): Either +} + +@JvmInline +value class NotBlankTrimmedString private constructor(val value: String) { + companion object : Exact by exactBuilder({ + it.mustBe(String::isNotBlank) + .transform(String::trim) + .build(::NotBlankTrimmedString) + }) +} diff --git a/src/commonMain/kotlin/arrow.exact/ExactDsl.kt b/src/commonMain/kotlin/arrow.exact/ExactDsl.kt index f1a5fbe..1f26d7a 100644 --- a/src/commonMain/kotlin/arrow.exact/ExactDsl.kt +++ b/src/commonMain/kotlin/arrow.exact/ExactDsl.kt @@ -1,15 +1,15 @@ package arrow.exact import arrow.core.* +import arrow.core.raise.Raise +import arrow.core.raise.either internal class AndExact( - private val exact1: Exact, - private val exact2: Exact + private val exact1: Exact, private val exact2: Exact ) : Exact { override fun from(value: A): Either { - return exact1.from(value) - .flatMap { exact2.from(it) } + return exact1.from(value).flatMap { exact2.from(it) } } } @@ -25,6 +25,13 @@ fun exact(predicate: Predicate, constructor: (A) -> B): Exact { } } +fun exact(constraint: Raise.(A) -> B): Exact { + return object : Exact { + override fun from(value: A): Either = either { constraint(value) } + } +} + + infix fun Exact.and(other: Exact): Exact { return AndExact(this, other) } From 5690e173555b94e8765657b09b9e3482319d3ba6 Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Fri, 5 May 2023 17:45:51 +0300 Subject: [PATCH 02/12] Add DSL marker --- ...ExactBuilderDsl.kt => ExactBuilderImpl.kt} | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) rename src/commonMain/kotlin/arrow.exact/{ExactBuilderDsl.kt => ExactBuilderImpl.kt} (82%) diff --git a/src/commonMain/kotlin/arrow.exact/ExactBuilderDsl.kt b/src/commonMain/kotlin/arrow.exact/ExactBuilderImpl.kt similarity index 82% rename from src/commonMain/kotlin/arrow.exact/ExactBuilderDsl.kt rename to src/commonMain/kotlin/arrow.exact/ExactBuilderImpl.kt index 1aa56d7..6068f04 100644 --- a/src/commonMain/kotlin/arrow.exact/ExactBuilderDsl.kt +++ b/src/commonMain/kotlin/arrow.exact/ExactBuilderImpl.kt @@ -5,32 +5,36 @@ import arrow.core.raise.Raise import arrow.core.raise.recover import kotlin.jvm.JvmInline +@DslMarker +annotation class ExactBuilderDsl + +@ExactBuilderDsl fun exactBuilder( block: (ExactBuilder) -> Either ): Exact { return object : Exact { override fun from(value: A): Either = - block(ExactBuilderDsl(value.right())) + block(ExactBuilderImpl(value.right())) } } -class ExactBuilderDsl( +private class ExactBuilderImpl( private val value: Either ) : ExactBuilder { - override fun mustBe(predicate: Predicate): ExactBuilder = ExactBuilderDsl( + override fun mustBe(predicate: Predicate): ExactBuilder = ExactBuilderImpl( value = value.flatMap { if (predicate(it)) it.right() else ExactError("Predicate failed").left() } ) - override fun transform(transformation: (A) -> B): ExactBuilder = ExactBuilderDsl( + override fun transform(transformation: (A) -> B): ExactBuilder = ExactBuilderImpl( value = value.map(transformation) ) override fun transformOrRaise( transformation: Raise.(A) -> B - ): ExactBuilder = ExactBuilderDsl( + ): ExactBuilder = ExactBuilderImpl( value = value.flatMap { recover({ transformation(it).right() }) { ExactError("Transform or raise failed").left() } } @@ -40,9 +44,16 @@ class ExactBuilderDsl( } interface ExactBuilder { + @ExactBuilderDsl fun mustBe(predicate: Predicate): ExactBuilder + + @ExactBuilderDsl fun transform(transformation: (A) -> B): ExactBuilder + + @ExactBuilderDsl fun transformOrRaise(transformation: Raise.(A) -> B): ExactBuilder + + @ExactBuilderDsl fun build(constructor: (A) -> Constraint): Either } From 8acee2591cc1d00c0dcdc4fd6fbae92e3397c3f1 Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Fri, 5 May 2023 18:34:29 +0300 Subject: [PATCH 03/12] Add tests and refactor --- build.gradle.kts | 1 + .../kotlin/arrow.exact/ExactBuilderImpl.kt | 24 +++---- .../kotlin/arrow/exact/ExactBuilderDslSpec.kt | 62 +++++++++++++++++++ 3 files changed, 70 insertions(+), 17 deletions(-) create mode 100644 src/commonTest/kotlin/arrow/exact/ExactBuilderDslSpec.kt diff --git a/build.gradle.kts b/build.gradle.kts index 64b3afd..ac1ffbf 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") } } diff --git a/src/commonMain/kotlin/arrow.exact/ExactBuilderImpl.kt b/src/commonMain/kotlin/arrow.exact/ExactBuilderImpl.kt index 6068f04..bebefdf 100644 --- a/src/commonMain/kotlin/arrow.exact/ExactBuilderImpl.kt +++ b/src/commonMain/kotlin/arrow.exact/ExactBuilderImpl.kt @@ -2,19 +2,18 @@ package arrow.exact import arrow.core.* import arrow.core.raise.Raise -import arrow.core.raise.recover -import kotlin.jvm.JvmInline +import arrow.core.raise.either @DslMarker annotation class ExactBuilderDsl @ExactBuilderDsl fun exactBuilder( - block: (ExactBuilder) -> Either + build: (ExactBuilder) -> Either ): Exact { return object : Exact { override fun from(value: A): Either = - block(ExactBuilderImpl(value.right())) + build(ExactBuilderImpl(value.right())) } } @@ -23,8 +22,8 @@ private class ExactBuilderImpl( ) : ExactBuilder { override fun mustBe(predicate: Predicate): ExactBuilder = ExactBuilderImpl( - value = value.flatMap { - if (predicate(it)) it.right() else ExactError("Predicate failed").left() + value = value.flatMap { a -> + if (predicate(a)) a.right() else ExactError("Predicate failed for value: $a").left() } ) @@ -35,8 +34,8 @@ private class ExactBuilderImpl( override fun transformOrRaise( transformation: Raise.(A) -> B ): ExactBuilder = ExactBuilderImpl( - value = value.flatMap { - recover({ transformation(it).right() }) { ExactError("Transform or raise failed").left() } + value = value.flatMap { a -> + either { transformation(a) } } ) @@ -56,12 +55,3 @@ interface ExactBuilder { @ExactBuilderDsl fun build(constructor: (A) -> Constraint): Either } - -@JvmInline -value class NotBlankTrimmedString private constructor(val value: String) { - companion object : Exact by exactBuilder({ - it.mustBe(String::isNotBlank) - .transform(String::trim) - .build(::NotBlankTrimmedString) - }) -} diff --git a/src/commonTest/kotlin/arrow/exact/ExactBuilderDslSpec.kt b/src/commonTest/kotlin/arrow/exact/ExactBuilderDslSpec.kt new file mode 100644 index 0000000..7b806cc --- /dev/null +++ b/src/commonTest/kotlin/arrow/exact/ExactBuilderDslSpec.kt @@ -0,0 +1,62 @@ +package arrow.exact + +import arrow.core.raise.ensureNotNull +import io.kotest.assertions.arrow.core.shouldBeRight +import io.kotest.core.spec.style.FreeSpec +import io.kotest.data.row +import io.kotest.datatest.withData +import io.kotest.matchers.booleans.shouldBeTrue +import kotlin.jvm.JvmInline + +sealed interface Artificial { + object Admin : Artificial + data class User(val id: Int) : Artificial +} + +@JvmInline +value class ArtificialExample private constructor(val value: Artificial) { + companion object : Exact by exactBuilder({ builder -> + builder.mustBe(String::isNotBlank) + .transform(String::trim) + .transformOrRaise { + when { + it.startsWith("a/") -> Artificial.Admin + it.startsWith("u/") -> { + val userId = it.drop(2).toIntOrNull() + ensureNotNull(userId) { ExactError("Invalid user id in: $it") } + Artificial.User(userId) + } + + else -> raise(ExactError("Unknown value: $it")) + } + } + .build(::ArtificialExample) + }) +} + +class ExactBuilderDslSpec : FreeSpec({ + "artificial user" { + val res = ArtificialExample.from("u/123") + + res shouldBeRight Artificial.User(123) + } + + "artificial admin" { + val res = ArtificialExample.from("a/") + + res shouldBeRight Artificial.Admin + } + + "invalid" - { + withData( + row(""), + row(" "), + row("Okay"), + row("u/Fail"), + ) { (string) -> + val res = ArtificialExample.from(string) + + res.isLeft().shouldBeTrue() + } + } +}) From 7544e44ae7fcd3ce10a6c89c3d752e2725dc392f Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Fri, 5 May 2023 18:45:45 +0300 Subject: [PATCH 04/12] Rename file --- .../arrow.exact/{ExactBuilderImpl.kt => ExactBuilderDsl.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/commonMain/kotlin/arrow.exact/{ExactBuilderImpl.kt => ExactBuilderDsl.kt} (100%) diff --git a/src/commonMain/kotlin/arrow.exact/ExactBuilderImpl.kt b/src/commonMain/kotlin/arrow.exact/ExactBuilderDsl.kt similarity index 100% rename from src/commonMain/kotlin/arrow.exact/ExactBuilderImpl.kt rename to src/commonMain/kotlin/arrow.exact/ExactBuilderDsl.kt From 6e84b5ff1454827253e9092425311286016a1ad3 Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Fri, 5 May 2023 19:08:06 +0300 Subject: [PATCH 05/12] Fix the unit tests --- build.gradle.kts | 5 ++++ .../kotlin/arrow/exact/ExactBuilderDslSpec.kt | 30 +++++++++++-------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index ac1ffbf..2bb4b3a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,6 +52,7 @@ kotlin { 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:kotest-runner-junit5:5.6.1") implementation("io.kotest.extensions:kotest-assertions-arrow:1.3.3") } } @@ -63,3 +64,7 @@ kotlin { } } } + +tasks.withType().configureEach { + useJUnitPlatform() +} diff --git a/src/commonTest/kotlin/arrow/exact/ExactBuilderDslSpec.kt b/src/commonTest/kotlin/arrow/exact/ExactBuilderDslSpec.kt index 7b806cc..0eebc11 100644 --- a/src/commonTest/kotlin/arrow/exact/ExactBuilderDslSpec.kt +++ b/src/commonTest/kotlin/arrow/exact/ExactBuilderDslSpec.kt @@ -1,11 +1,13 @@ package arrow.exact +import arrow.core.Either import arrow.core.raise.ensureNotNull -import io.kotest.assertions.arrow.core.shouldBeRight +import io.kotest.assertions.arrow.core.shouldBeLeft +import io.kotest.assertions.failure import io.kotest.core.spec.style.FreeSpec import io.kotest.data.row import io.kotest.datatest.withData -import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.shouldBe import kotlin.jvm.JvmInline sealed interface Artificial { @@ -14,8 +16,8 @@ sealed interface Artificial { } @JvmInline -value class ArtificialExample private constructor(val value: Artificial) { - companion object : Exact by exactBuilder({ builder -> +value class ArtificialConstraintType private constructor(val value: Artificial) { + companion object : Exact by exactBuilder({ builder -> builder.mustBe(String::isNotBlank) .transform(String::trim) .transformOrRaise { @@ -30,21 +32,23 @@ value class ArtificialExample private constructor(val value: Artificial) { else -> raise(ExactError("Unknown value: $it")) } } - .build(::ArtificialExample) + .build(::ArtificialConstraintType) }) } class ExactBuilderDslSpec : FreeSpec({ "artificial user" { - val res = ArtificialExample.from("u/123") - - res shouldBeRight Artificial.User(123) + when (val res = ArtificialConstraintType.from("u/123")) { + is Either.Left -> failure(res.value.message) + is Either.Right -> res.value.value shouldBe Artificial.User(123) + } } "artificial admin" { - val res = ArtificialExample.from("a/") - - res shouldBeRight Artificial.Admin + when (val res = ArtificialConstraintType.from("a/")) { + is Either.Left -> failure(res.value.message) + is Either.Right -> res.value.value shouldBe Artificial.Admin + } } "invalid" - { @@ -54,9 +58,9 @@ class ExactBuilderDslSpec : FreeSpec({ row("Okay"), row("u/Fail"), ) { (string) -> - val res = ArtificialExample.from(string) + val res = ArtificialConstraintType.from(string) - res.isLeft().shouldBeTrue() + res.shouldBeLeft() } } }) From 65b5c89525d50adf964a8c7de3efc8b2e54513a9 Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Sat, 6 May 2023 22:20:40 +0300 Subject: [PATCH 06/12] Fix the unit tests setup --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 2bb4b3a..9239bc2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,7 +52,6 @@ kotlin { 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:kotest-runner-junit5:5.6.1") implementation("io.kotest.extensions:kotest-assertions-arrow:1.3.3") } } @@ -60,6 +59,7 @@ 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") } } } From 50aa23434fafbc784fa57fd241c20abe7123d6bc Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Sat, 6 May 2023 23:07:15 +0300 Subject: [PATCH 07/12] WIP: Re-worked Exact core --- src/commonMain/kotlin/arrow.exact/Exact.kt | 32 +++++---- .../kotlin/arrow.exact/ExactBuilderDsl.kt | 57 ---------------- src/commonMain/kotlin/arrow.exact/ExactDsl.kt | 40 ++++------- .../kotlin/arrow.exact/demo/Demo.kt | 24 +++++++ .../kotlin/arrow/exact/ExactBuilderDslSpec.kt | 66 ------------------- .../kotlin/arrow/exact/ExactSpec.kt | 37 ----------- src/commonTest/kotlin/arrow/exact/Strings.kt | 21 ------ 7 files changed, 57 insertions(+), 220 deletions(-) delete mode 100644 src/commonMain/kotlin/arrow.exact/ExactBuilderDsl.kt create mode 100644 src/commonMain/kotlin/arrow.exact/demo/Demo.kt delete mode 100644 src/commonTest/kotlin/arrow/exact/ExactBuilderDslSpec.kt delete mode 100644 src/commonTest/kotlin/arrow/exact/ExactSpec.kt delete mode 100644 src/commonTest/kotlin/arrow/exact/Strings.kt diff --git a/src/commonMain/kotlin/arrow.exact/Exact.kt b/src/commonMain/kotlin/arrow.exact/Exact.kt index 4829d37..db3bb43 100644 --- a/src/commonMain/kotlin/arrow.exact/Exact.kt +++ b/src/commonMain/kotlin/arrow.exact/Exact.kt @@ -1,23 +1,31 @@ package arrow.exact import arrow.core.Either +import arrow.core.raise.Raise +import arrow.core.raise.either -interface Exact { - fun from(value: A): Either +interface Exact> : ExactWithError - fun fromOrNull(value: A): B? { - return from(value).getOrNull() - } +data class ExactError(val message: String) + +interface ExactWithError> { + + fun Raise.from(value: A): R + + fun fromOrNull(value: A): R? = either { 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 - } + fun fromOrThrow(value: A): R = when (val result = either { from(value) }) { + is Either.Left -> throw ExactException(result.value) + is Either.Right -> result.value } + + operator fun invoke(value: A): R = fromOrThrow(value) } -class ExactError(val message: String) +class ExactException(error: Any) : IllegalArgumentException("ArrowExact error: $error") + +interface Refined { + val value: A +} -class ExactException(message: String) : IllegalArgumentException(message) diff --git a/src/commonMain/kotlin/arrow.exact/ExactBuilderDsl.kt b/src/commonMain/kotlin/arrow.exact/ExactBuilderDsl.kt deleted file mode 100644 index bebefdf..0000000 --- a/src/commonMain/kotlin/arrow.exact/ExactBuilderDsl.kt +++ /dev/null @@ -1,57 +0,0 @@ -package arrow.exact - -import arrow.core.* -import arrow.core.raise.Raise -import arrow.core.raise.either - -@DslMarker -annotation class ExactBuilderDsl - -@ExactBuilderDsl -fun exactBuilder( - build: (ExactBuilder) -> Either -): Exact { - return object : Exact { - override fun from(value: A): Either = - build(ExactBuilderImpl(value.right())) - } -} - -private class ExactBuilderImpl( - private val value: Either -) : ExactBuilder { - - override fun mustBe(predicate: Predicate): ExactBuilder = ExactBuilderImpl( - value = value.flatMap { a -> - if (predicate(a)) a.right() else ExactError("Predicate failed for value: $a").left() - } - ) - - override fun transform(transformation: (A) -> B): ExactBuilder = ExactBuilderImpl( - value = value.map(transformation) - ) - - override fun transformOrRaise( - transformation: Raise.(A) -> B - ): ExactBuilder = ExactBuilderImpl( - value = value.flatMap { a -> - either { transformation(a) } - } - ) - - override fun build(constructor: (A) -> Constraint): Either = value.map(constructor) -} - -interface ExactBuilder { - @ExactBuilderDsl - fun mustBe(predicate: Predicate): ExactBuilder - - @ExactBuilderDsl - fun transform(transformation: (A) -> B): ExactBuilder - - @ExactBuilderDsl - fun transformOrRaise(transformation: Raise.(A) -> B): ExactBuilder - - @ExactBuilderDsl - fun build(constructor: (A) -> Constraint): Either -} diff --git a/src/commonMain/kotlin/arrow.exact/ExactDsl.kt b/src/commonMain/kotlin/arrow.exact/ExactDsl.kt index 1f26d7a..6d0302c 100644 --- a/src/commonMain/kotlin/arrow.exact/ExactDsl.kt +++ b/src/commonMain/kotlin/arrow.exact/ExactDsl.kt @@ -1,37 +1,23 @@ 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) } - } -} - -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() - } - } - } -} - -fun exact(constraint: Raise.(A) -> B): Exact { - return object : Exact { - override fun from(value: A): Either = either { constraint(value) } - } +@ExactDsl +fun > exact( + construct: Raise.(A) -> R +): Exact = object : Exact { + override fun from(value: A): Either = either { construct(value) } } -infix fun Exact.and(other: Exact): Exact { - return AndExact(this, other) +@ExactDsl +fun > exactWithError( + construct: Raise.(A) -> R +): ExactWithError = object : ExactWithError { + 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..f9d9154 --- /dev/null +++ b/src/commonMain/kotlin/arrow.exact/demo/Demo.kt @@ -0,0 +1,24 @@ +package arrow.exact.demo + +import arrow.core.raise.ensureNotNull +import arrow.exact.Exact +import arrow.exact.ExactError +import arrow.exact.Refined +import arrow.exact.exact +import kotlin.jvm.JvmInline + +@JvmInline +value class NotBlankTrimmedString private constructor( + override val value: String +) : Refined { + companion object : Exact by exact({ + val trimmed = it.trim().takeIf(String::isNotBlank) + ensureNotNull(trimmed) { ExactError("Cannot be blank.") } + NotBlankTrimmedString(trimmed) + }) +} + +fun main() { + val str = NotBlankTrimmedString("Hello") + +} diff --git a/src/commonTest/kotlin/arrow/exact/ExactBuilderDslSpec.kt b/src/commonTest/kotlin/arrow/exact/ExactBuilderDslSpec.kt deleted file mode 100644 index 0eebc11..0000000 --- a/src/commonTest/kotlin/arrow/exact/ExactBuilderDslSpec.kt +++ /dev/null @@ -1,66 +0,0 @@ -package arrow.exact - -import arrow.core.Either -import arrow.core.raise.ensureNotNull -import io.kotest.assertions.arrow.core.shouldBeLeft -import io.kotest.assertions.failure -import io.kotest.core.spec.style.FreeSpec -import io.kotest.data.row -import io.kotest.datatest.withData -import io.kotest.matchers.shouldBe -import kotlin.jvm.JvmInline - -sealed interface Artificial { - object Admin : Artificial - data class User(val id: Int) : Artificial -} - -@JvmInline -value class ArtificialConstraintType private constructor(val value: Artificial) { - companion object : Exact by exactBuilder({ builder -> - builder.mustBe(String::isNotBlank) - .transform(String::trim) - .transformOrRaise { - when { - it.startsWith("a/") -> Artificial.Admin - it.startsWith("u/") -> { - val userId = it.drop(2).toIntOrNull() - ensureNotNull(userId) { ExactError("Invalid user id in: $it") } - Artificial.User(userId) - } - - else -> raise(ExactError("Unknown value: $it")) - } - } - .build(::ArtificialConstraintType) - }) -} - -class ExactBuilderDslSpec : FreeSpec({ - "artificial user" { - when (val res = ArtificialConstraintType.from("u/123")) { - is Either.Left -> failure(res.value.message) - is Either.Right -> res.value.value shouldBe Artificial.User(123) - } - } - - "artificial admin" { - when (val res = ArtificialConstraintType.from("a/")) { - is Either.Left -> failure(res.value.message) - is Either.Right -> res.value.value shouldBe Artificial.Admin - } - } - - "invalid" - { - withData( - row(""), - row(" "), - row("Okay"), - row("u/Fail"), - ) { (string) -> - val res = ArtificialConstraintType.from(string) - - res.shouldBeLeft() - } - } -}) 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 -} From 9cf25cc40f4feffad63d6962db3218339786adf6 Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Sat, 6 May 2023 23:08:48 +0300 Subject: [PATCH 08/12] Implement Exact core (no extensions) --- src/commonMain/kotlin/arrow.exact/ExactDsl.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/commonMain/kotlin/arrow.exact/ExactDsl.kt b/src/commonMain/kotlin/arrow.exact/ExactDsl.kt index 6d0302c..cdbcb6d 100644 --- a/src/commonMain/kotlin/arrow.exact/ExactDsl.kt +++ b/src/commonMain/kotlin/arrow.exact/ExactDsl.kt @@ -1,8 +1,6 @@ package arrow.exact -import arrow.core.Either import arrow.core.raise.Raise -import arrow.core.raise.either @DslMarker annotation class ExactDsl @@ -11,7 +9,7 @@ annotation class ExactDsl fun > exact( construct: Raise.(A) -> R ): Exact = object : Exact { - override fun from(value: A): Either = either { construct(value) } + override fun Raise.from(value: A): R = construct(value) } @@ -19,5 +17,5 @@ fun > exact( fun > exactWithError( construct: Raise.(A) -> R ): ExactWithError = object : ExactWithError { - override fun from(value: A): Either = either { construct(value) } + override fun Raise.from(value: A): R = construct(value) } From e5211f580a4a6217de40bb6ed7cf94a4f6d810a7 Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Sat, 6 May 2023 23:17:17 +0300 Subject: [PATCH 09/12] Fix the core --- src/commonMain/kotlin/arrow.exact/Exact.kt | 13 +++++----- src/commonMain/kotlin/arrow.exact/ExactDsl.kt | 11 ++++---- .../kotlin/arrow.exact/demo/Demo.kt | 26 +++++++++++++++---- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/src/commonMain/kotlin/arrow.exact/Exact.kt b/src/commonMain/kotlin/arrow.exact/Exact.kt index db3bb43..f7b270e 100644 --- a/src/commonMain/kotlin/arrow.exact/Exact.kt +++ b/src/commonMain/kotlin/arrow.exact/Exact.kt @@ -2,20 +2,21 @@ package arrow.exact import arrow.core.Either import arrow.core.raise.Raise -import arrow.core.raise.either -interface Exact> : ExactWithError +interface Exact> : ExactEither data class ExactError(val message: String) -interface ExactWithError> { +interface ExactEither> { - fun Raise.from(value: A): R + fun from(value: A): Either - fun fromOrNull(value: A): R? = either { from(value) }.getOrNull() + fun Raise.fromOrRaise(value: A): R = from(value).bind() - fun fromOrThrow(value: A): R = when (val result = either { from(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 } diff --git a/src/commonMain/kotlin/arrow.exact/ExactDsl.kt b/src/commonMain/kotlin/arrow.exact/ExactDsl.kt index cdbcb6d..273fded 100644 --- a/src/commonMain/kotlin/arrow.exact/ExactDsl.kt +++ b/src/commonMain/kotlin/arrow.exact/ExactDsl.kt @@ -1,6 +1,8 @@ package arrow.exact +import arrow.core.Either import arrow.core.raise.Raise +import arrow.core.raise.either @DslMarker annotation class ExactDsl @@ -9,13 +11,12 @@ annotation class ExactDsl fun > exact( construct: Raise.(A) -> R ): Exact = object : Exact { - override fun Raise.from(value: A): R = construct(value) + override fun from(value: A): Either = either { construct(value) } } - @ExactDsl -fun > exactWithError( +fun > exactEither( construct: Raise.(A) -> R -): ExactWithError = object : ExactWithError { - override fun Raise.from(value: A): R = construct(value) +): 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 index f9d9154..ccf5a5a 100644 --- a/src/commonMain/kotlin/arrow.exact/demo/Demo.kt +++ b/src/commonMain/kotlin/arrow.exact/demo/Demo.kt @@ -1,10 +1,8 @@ package arrow.exact.demo +import arrow.core.raise.ensure import arrow.core.raise.ensureNotNull -import arrow.exact.Exact -import arrow.exact.ExactError -import arrow.exact.Refined -import arrow.exact.exact +import arrow.exact.* import kotlin.jvm.JvmInline @JvmInline @@ -18,7 +16,25 @@ value class NotBlankTrimmedString private constructor( }) } +sealed interface UsernameError { + object Invalid : UsernameError + data class Offensive(val username: String) : UsernameError +} + +@JvmInline +value class Username private constructor( + override val value: String +) : Refined { + companion object : ExactEither by exactEither({ + ensure(it.isNotBlank() && it.length < 100) { UsernameError.Invalid } + ensure(it !in listOf("offensive")) { UsernameError.Offensive(it) } + Username(it.trim()) + }) +} + fun main() { - val str = NotBlankTrimmedString("Hello") + val hello = NotBlankTrimmedString("Hello") + val world = NotBlankTrimmedString("World") + val helloWorld = NotBlankTrimmedString.fromOrNull(hello.value + " " + world.value) } From c78e69d971d130b26163b44061edb09aa5b083dd Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Sat, 6 May 2023 23:32:15 +0300 Subject: [PATCH 10/12] Cleanup and shape --- src/commonMain/kotlin/arrow.exact/Exact.kt | 5 +++-- src/commonMain/kotlin/arrow.exact/demo/Demo.kt | 12 ++++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/commonMain/kotlin/arrow.exact/Exact.kt b/src/commonMain/kotlin/arrow.exact/Exact.kt index f7b270e..d95ddfe 100644 --- a/src/commonMain/kotlin/arrow.exact/Exact.kt +++ b/src/commonMain/kotlin/arrow.exact/Exact.kt @@ -1,7 +1,6 @@ package arrow.exact import arrow.core.Either -import arrow.core.raise.Raise interface Exact> : ExactEither @@ -12,7 +11,8 @@ interface ExactEither> { fun from(value: A): Either - fun Raise.fromOrRaise(value: A): R = from(value).bind() + // TODO: This doesn't work for some weird reason :/ + // fun Raise.fromOrRaise(value: A): R = from(value).bind() fun fromOrNull(value: A): R? = from(value).getOrNull() @@ -21,6 +21,7 @@ interface ExactEither> { is Either.Right -> result.value } + // TODO: What are your thoughts about this? operator fun invoke(value: A): R = fromOrThrow(value) } diff --git a/src/commonMain/kotlin/arrow.exact/demo/Demo.kt b/src/commonMain/kotlin/arrow.exact/demo/Demo.kt index ccf5a5a..4164793 100644 --- a/src/commonMain/kotlin/arrow.exact/demo/Demo.kt +++ b/src/commonMain/kotlin/arrow.exact/demo/Demo.kt @@ -1,10 +1,13 @@ package arrow.exact.demo +import arrow.core.Either +import arrow.core.raise.either import arrow.core.raise.ensure import arrow.core.raise.ensureNotNull import arrow.exact.* import kotlin.jvm.JvmInline +// TODO: We need a lint check telling people to make their constructors private @JvmInline value class NotBlankTrimmedString private constructor( override val value: String @@ -32,9 +35,14 @@ value class Username private constructor( }) } -fun main() { +fun demo(): Either = either { val hello = NotBlankTrimmedString("Hello") val world = NotBlankTrimmedString("World") - val helloWorld = NotBlankTrimmedString.fromOrNull(hello.value + " " + world.value) + val helloWorld = NotBlankTrimmedString.from(hello.value + " " + world.value) + .mapLeft { it.message }.bind() + val username = Username.from("user1") + .mapLeft { it.toString() }.bind() + + hello } From 8c620fd48aea17477e886b1fd9c10736f7c81397 Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Sat, 6 May 2023 23:56:34 +0300 Subject: [PATCH 11/12] Exact core 0.0.1 --- src/commonMain/kotlin/arrow.exact/ExactDsl.kt | 4 ++ .../kotlin/arrow.exact/demo/Demo.kt | 51 +++++++++++++++---- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/src/commonMain/kotlin/arrow.exact/ExactDsl.kt b/src/commonMain/kotlin/arrow.exact/ExactDsl.kt index 273fded..672539e 100644 --- a/src/commonMain/kotlin/arrow.exact/ExactDsl.kt +++ b/src/commonMain/kotlin/arrow.exact/ExactDsl.kt @@ -20,3 +20,7 @@ fun > exactEither( ): ExactEither = object : ExactEither { override fun from(value: A): Either = either { construct(value) } } + +// TODO: Add any relevant extensions to Refined + +fun Refined.map(f: (A) -> B): B = f(value) diff --git a/src/commonMain/kotlin/arrow.exact/demo/Demo.kt b/src/commonMain/kotlin/arrow.exact/demo/Demo.kt index 4164793..f8bcb67 100644 --- a/src/commonMain/kotlin/arrow.exact/demo/Demo.kt +++ b/src/commonMain/kotlin/arrow.exact/demo/Demo.kt @@ -1,21 +1,33 @@ 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.raise.ensureNotNull +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( + override val value: String +) : Refined { + 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( override val value: String ) : Refined { - companion object : Exact by exact({ - val trimmed = it.trim().takeIf(String::isNotBlank) - ensureNotNull(trimmed) { ExactError("Cannot be blank.") } - NotBlankTrimmedString(trimmed) + companion object : Exact by exact({ raw -> + val notBlank = NotBlankString.from(raw).bind() + NotBlankTrimmedString(notBlank.value.trim()) }) } @@ -28,14 +40,26 @@ sealed interface UsernameError { value class Username private constructor( override val value: String ) : Refined { - companion object : ExactEither by exactEither({ - ensure(it.isNotBlank() && it.length < 100) { UsernameError.Invalid } - ensure(it !in listOf("offensive")) { UsernameError.Offensive(it) } - Username(it.trim()) + 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( + override val value: Int +) : Refined { + companion object : Exact by exact({ + ensure(it > 0) { ExactError("Must be positive.") } + PositiveInt(it) }) } -fun demo(): Either = either { +fun demo(): Either = either { val hello = NotBlankTrimmedString("Hello") val world = NotBlankTrimmedString("World") @@ -44,5 +68,10 @@ fun demo(): Either = either { val username = Username.from("user1") .mapLeft { it.toString() }.bind() - hello + + val x = PositiveInt(3) + val y = PositiveInt.from(Random.nextInt()) + val z = y.flatMap { y1 -> + PositiveInt(x.value + y1.value).right() + } } From 0334c7233f354b3c3558d19d8ae3e5251f7e6503 Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Mon, 8 May 2023 11:24:39 +0300 Subject: [PATCH 12/12] Remove `Refined` --- src/commonMain/kotlin/arrow.exact/Exact.kt | 9 ++------- src/commonMain/kotlin/arrow.exact/ExactDsl.kt | 8 ++------ src/commonMain/kotlin/arrow.exact/demo/Demo.kt | 16 ++++++++-------- 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/commonMain/kotlin/arrow.exact/Exact.kt b/src/commonMain/kotlin/arrow.exact/Exact.kt index d95ddfe..875777a 100644 --- a/src/commonMain/kotlin/arrow.exact/Exact.kt +++ b/src/commonMain/kotlin/arrow.exact/Exact.kt @@ -3,11 +3,11 @@ package arrow.exact import arrow.core.Either -interface Exact> : ExactEither +interface Exact : ExactEither data class ExactError(val message: String) -interface ExactEither> { +interface ExactEither { fun from(value: A): Either @@ -26,8 +26,3 @@ interface ExactEither> { } class ExactException(error: Any) : IllegalArgumentException("ArrowExact error: $error") - -interface Refined { - val value: A -} - diff --git a/src/commonMain/kotlin/arrow.exact/ExactDsl.kt b/src/commonMain/kotlin/arrow.exact/ExactDsl.kt index 672539e..0130e1e 100644 --- a/src/commonMain/kotlin/arrow.exact/ExactDsl.kt +++ b/src/commonMain/kotlin/arrow.exact/ExactDsl.kt @@ -8,19 +8,15 @@ import arrow.core.raise.either annotation class ExactDsl @ExactDsl -fun > exact( +fun exact( construct: Raise.(A) -> R ): Exact = object : Exact { override fun from(value: A): Either = either { construct(value) } } @ExactDsl -fun > exactEither( +fun exactEither( construct: Raise.(A) -> R ): ExactEither = object : ExactEither { override fun from(value: A): Either = either { construct(value) } } - -// TODO: Add any relevant extensions to Refined - -fun Refined.map(f: (A) -> B): B = f(value) diff --git a/src/commonMain/kotlin/arrow.exact/demo/Demo.kt b/src/commonMain/kotlin/arrow.exact/demo/Demo.kt index f8bcb67..c1c238a 100644 --- a/src/commonMain/kotlin/arrow.exact/demo/Demo.kt +++ b/src/commonMain/kotlin/arrow.exact/demo/Demo.kt @@ -12,8 +12,8 @@ import kotlin.random.Random // TODO: We need a lint check telling people to make their constructors private @JvmInline value class NotBlankString private constructor( - override val value: String -) : Refined { + val value: String +) { companion object : Exact by exact({ ensure(it.isNotBlank()) { ExactError("Cannot be blank.") } NotBlankString(it) @@ -23,8 +23,8 @@ value class NotBlankString private constructor( // TODO: We need a lint check telling people to make their constructors private @JvmInline value class NotBlankTrimmedString private constructor( - override val value: String -) : Refined { + val value: String +) { companion object : Exact by exact({ raw -> val notBlank = NotBlankString.from(raw).bind() NotBlankTrimmedString(notBlank.value.trim()) @@ -38,8 +38,8 @@ sealed interface UsernameError { @JvmInline value class Username private constructor( - override val value: String -) : Refined { + val value: String +) { companion object : ExactEither by exactEither({ rawUsername -> val username = NotBlankTrimmedString.from(rawUsername) // compose Exact .mapLeft { UsernameError.Invalid }.bind().value @@ -51,8 +51,8 @@ value class Username private constructor( @JvmInline value class PositiveInt private constructor( - override val value: Int -) : Refined { + val value: Int +) { companion object : Exact by exact({ ensure(it > 0) { ExactError("Must be positive.") } PositiveInt(it)