Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DRAFT] Exact Builder Dsl #7

Closed
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ 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:kotest-runner-junit5:5.6.1")
ILIYANGERMANOV marked this conversation as resolved.
Show resolved Hide resolved
implementation("io.kotest.extensions:kotest-assertions-arrow:1.3.3")
}
}
Expand All @@ -62,3 +64,7 @@ kotlin {
}
}
}

tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
4 changes: 2 additions & 2 deletions src/commonMain/kotlin/arrow.exact/Exact.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package arrow.exact

import arrow.core.*
import arrow.core.Either

interface Exact<A, out B> {

Expand All @@ -18,6 +18,6 @@ interface Exact<A, out B> {
}
}

open class ExactError(val message: String)
class ExactError(val message: String)

class ExactException(message: String) : IllegalArgumentException(message)
57 changes: 57 additions & 0 deletions src/commonMain/kotlin/arrow.exact/ExactBuilderDsl.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package arrow.exact

import arrow.core.*
import arrow.core.raise.Raise
import arrow.core.raise.either

@DslMarker
annotation class ExactBuilderDsl

@ExactBuilderDsl
fun <A, Constraint> exactBuilder(
build: (ExactBuilder<A, Constraint>) -> Either<ExactError, Constraint>
): Exact<A, Constraint> {
return object : Exact<A, Constraint> {
override fun from(value: A): Either<ExactError, Constraint> =
build(ExactBuilderImpl(value.right()))
}
}

private class ExactBuilderImpl<A, Constraint>(
private val value: Either<ExactError, A>
) : ExactBuilder<A, Constraint> {

override fun mustBe(predicate: Predicate<A>): ExactBuilder<A, Constraint> = ExactBuilderImpl(
value = value.flatMap { a ->
if (predicate(a)) a.right() else ExactError("Predicate failed for value: $a").left()
}
)

override fun <B> transform(transformation: (A) -> B): ExactBuilder<B, Constraint> = ExactBuilderImpl(
value = value.map(transformation)
)

override fun <B> transformOrRaise(
transformation: Raise<ExactError>.(A) -> B
): ExactBuilder<B, Constraint> = ExactBuilderImpl(
value = value.flatMap { a ->
either { transformation(a) }
}
)

override fun build(constructor: (A) -> Constraint): Either<ExactError, Constraint> = value.map(constructor)
}

interface ExactBuilder<A, Constraint> {
@ExactBuilderDsl
fun mustBe(predicate: Predicate<A>): ExactBuilder<A, Constraint>

@ExactBuilderDsl
fun <B> transform(transformation: (A) -> B): ExactBuilder<B, Constraint>

@ExactBuilderDsl
fun <B> transformOrRaise(transformation: Raise<ExactError>.(A) -> B): ExactBuilder<B, Constraint>

@ExactBuilderDsl
fun build(constructor: (A) -> Constraint): Either<ExactError, Constraint>
}
15 changes: 11 additions & 4 deletions src/commonMain/kotlin/arrow.exact/ExactDsl.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package arrow.exact

import arrow.core.*
import arrow.core.raise.Raise
import arrow.core.raise.either

internal class AndExact<A, B, C>(
private val exact1: Exact<A, B>,
private val exact2: Exact<B, C>
private val exact1: Exact<A, B>, private val exact2: Exact<B, C>
) : Exact<A, C> {

override fun from(value: A): Either<ExactError, C> {
return exact1.from(value)
.flatMap { exact2.from(it) }
return exact1.from(value).flatMap { exact2.from(it) }
}
}

Expand All @@ -25,6 +25,13 @@ fun <A, B> exact(predicate: Predicate<A>, constructor: (A) -> B): Exact<A, B> {
}
}

fun <A, B> exact(constraint: Raise<ExactError>.(A) -> B): Exact<A, B> {
return object : Exact<A, B> {
override fun from(value: A): Either<ExactError, B> = either { constraint(value) }
}
}


infix fun <A, B, C> Exact<A, B>.and(other: Exact<B, C>): Exact<A, C> {
return AndExact(this, other)
}
66 changes: 66 additions & 0 deletions src/commonTest/kotlin/arrow/exact/ExactBuilderDslSpec.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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<String, ArtificialConstraintType> 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()
}
}
})