Skip to content

Commit

Permalink
Add some alternate syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
dhoepelman committed Nov 15, 2024
1 parent 0027c40 commit 3f89865
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 10 deletions.
14 changes: 14 additions & 0 deletions src/commonMain/kotlin/io/konform/validation/ValidationBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import io.konform.validation.path.ValidationPath
import io.konform.validation.types.ArrayValidation
import io.konform.validation.types.CallableValidation
import io.konform.validation.types.ConstraintsValidation
import io.konform.validation.types.DynamicCallableValidation
import io.konform.validation.types.DynamicValidation
import io.konform.validation.types.IsClassValidation
import io.konform.validation.types.IterableValidation
Expand Down Expand Up @@ -125,6 +126,10 @@ public open class ValidationBuilder<T> {

public infix fun <R> KFunction1<T, R?>.required(init: ValidationBuilder<R>.() -> Unit): Unit = required(this, this, init)

public infix fun <R> KProperty1<T, R>.dynamic(init: ValidationBuilder<R>.(T) -> Unit): Unit = dynamic(this, this, init)

public infix fun <R> KFunction1<T, R>.dynamic(init: ValidationBuilder<R>.(T) -> Unit): Unit = dynamic(this, this, init)

/**
* Calculate a value from the input and run a validation on it.
* @param path The [PathSegment] or [ValidationPath] of the validation.
Expand All @@ -137,6 +142,15 @@ public open class ValidationBuilder<T> {
init: ValidationBuilder<R>.() -> Unit,
): Unit = run(CallableValidation(path, f, buildWithNew(init)))

public fun <R> dynamic(
path: Any,
f: (T) -> R,
init: ValidationBuilder<R>.(T) -> Unit,
): Unit = run(DynamicCallableValidation(ValidationPath.of(path), f, init))

/** Build a new validation based on the current value being validated and run it. */
public fun dynamic(init: ValidationBuilder<T>.(T) -> Unit): Unit = dynamic(ValidationPath.EMPTY, { it }, init)

/**
* Calculate a value from the input and run a validation on it, but only if the value is not null.
* @param path The [PathSegment] or [ValidationPath] of the validation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ public data class ValidationError(

public inline fun mapPath(f: (List<PathSegment>) -> List<PathSegment>): ValidationError = copy(path = ValidationPath(f(path.segments)))

internal fun prependPath(path: ValidationPath) = copy(path = this.path.prepend(path))
public fun prependPath(path: ValidationPath): ValidationError = copy(path = this.path.prepend(path))

internal fun prependPath(pathSegment: PathSegment) = mapPath { it.prepend(pathSegment) }
public fun prependPath(pathSegment: PathSegment): ValidationError = mapPath { it.prepend(pathSegment) }

internal companion object {
internal fun of(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public data class ValidationPath(
override fun toString(): String = "ValidationPath(${segments.joinToString(", ")})"

public companion object {
internal val EMPTY = ValidationPath(emptyList())
public val EMPTY: ValidationPath = ValidationPath(emptyList())

/**
* Convert the specified arguments into a [ValidationPath]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package io.konform.validation.types

import io.konform.validation.Invalid
import io.konform.validation.Valid
import io.konform.validation.Validation
import io.konform.validation.ValidationBuilder
import io.konform.validation.ValidationResult
import io.konform.validation.path.ValidationPath

internal class DynamicValidation<T>(
private val creator: (T) -> Validation<T>,
Expand All @@ -11,3 +15,22 @@ internal class DynamicValidation<T>(
return validation.validate(value)
}
}

internal class DynamicCallableValidation<T, R>(
private val path: ValidationPath,
private val callable: (T) -> R,
private val builder: ValidationBuilder<R>.(T) -> Unit,
) : Validation<T> {
override fun validate(value: T): ValidationResult<T> {
val validation =
ValidationBuilder<R>()
.also {
builder(it, value)
}.build()
val toValidate = callable(value)
return when (val callableResult = validation(toValidate)) {
is Valid -> Valid(value)
is Invalid -> callableResult.prependPath(path)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.konform.validation.validationbuilder

import io.konform.validation.Constraint
import io.konform.validation.Validation
import io.konform.validation.ValidationBuilder
import io.konform.validation.ValidationError
import io.konform.validation.constraints.pattern
import io.kotest.assertions.konform.shouldBeInvalid
Expand All @@ -9,10 +11,11 @@ import io.kotest.assertions.konform.shouldContainOnlyError
import kotlin.test.Test

class DynamicValidationTest {
val validation =
Validation<Address> {
runDynamic { address: Address ->
Validation<Address> {
@Test
fun dynamicValidation1() {
val validation =
Validation<Address> {
dynamic { address ->
Address::postalCode {
when (address.countryCode) {
"US" -> pattern("[0-9]{5}")
Expand All @@ -21,10 +24,7 @@ class DynamicValidationTest {
}
}
}
}

@Test
fun dynamicValidation() {
validation shouldBeValid Address("US", "12345")
validation shouldBeValid Address("DE", "ABC")

Expand All @@ -33,9 +33,71 @@ class DynamicValidationTest {
(validation shouldBeInvalid Address("DE", "123")) shouldContainOnlyError
ValidationError.of(Address::postalCode, """must match pattern '[A-Z]+'""")
}

@Test
fun dynamicValidation2() {
val validation =
Validation<Range> {
dynamic { range ->
Range::to {
largerThan(range.from)
}
}
}

validation shouldBeValid Range(0, 1)
(validation shouldBeInvalid Range(1, 0)) shouldContainOnlyError
ValidationError.of(
Range::to,
"must be larger than 1",
)
}

@Test
fun dynamicOnProperty() {
val validation =
Validation<Range> {
Range::to dynamic { range ->
largerThan(range.from)
}
}

validation shouldBeValid Range(0, 1)
(validation shouldBeInvalid Range(1, 0)) shouldContainOnlyError
ValidationError.of(
Range::to,
"must be larger than 1",
)
}

@Test
fun dynamicWithLambda() {
val validation =
Validation<Range> {
dynamic(Range::to, { it.from to it.to }) { (from, to) ->
constrain("must be larger than from") {
to > from
}
}
}

validation shouldBeValid Range(0, 1)
(validation shouldBeInvalid Range(1, 0)) shouldContainOnlyError
ValidationError.of(
Range::to,
"must be larger than from",
)
}
}

data class Address(
val countryCode: String,
val postalCode: String,
)

data class Range(
val from: Int,
val to: Int,
)

fun ValidationBuilder<Int>.largerThan(other: Int): Constraint<Int> = constrain("must be larger than $other") { it > other }

0 comments on commit 3f89865

Please sign in to comment.