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

Add function and lambda accessor syntax #65

Merged
merged 12 commits into from
Sep 10, 2024
66 changes: 51 additions & 15 deletions src/commonMain/kotlin/io/konform/validation/ValidationBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import io.konform.validation.internal.OptionalValidation
import io.konform.validation.internal.RequiredValidation
import io.konform.validation.internal.ValidationBuilderImpl
import kotlin.jvm.JvmName
import kotlin.reflect.KFunction1
import kotlin.reflect.KProperty1

@DslMarker
Expand All @@ -24,40 +25,76 @@ public abstract class ValidationBuilder<T> {

public abstract infix fun Constraint<T>.hint(hint: String): Constraint<T>

public abstract operator fun <R> KProperty1<T, R>.invoke(init: ValidationBuilder<R>.() -> Unit)
public abstract operator fun <R> ((T) -> R).invoke(
name: String,
init: ValidationBuilder<R>.() -> Unit,
)

internal abstract fun <R> onEachIterable(
prop: KProperty1<T, Iterable<R>>,
name: String,
prop: (T) -> Iterable<R>,
init: ValidationBuilder<R>.() -> Unit,
)

@JvmName("onEachIterable")
public infix fun <R> KProperty1<T, Iterable<R>>.onEach(init: ValidationBuilder<R>.() -> Unit): Unit = onEachIterable(this, init)

internal abstract fun <R> onEachArray(
prop: KProperty1<T, Array<R>>,
name: String,
prop: (T) -> Array<R>,
init: ValidationBuilder<R>.() -> Unit,
)

@JvmName("onEachArray")
public infix fun <R> KProperty1<T, Array<R>>.onEach(init: ValidationBuilder<R>.() -> Unit): Unit = onEachArray(this, init)

internal abstract fun <K, V> onEachMap(
prop: KProperty1<T, Map<K, V>>,
name: String,
prop: (T) -> Map<K, V>,
init: ValidationBuilder<Map.Entry<K, V>>.() -> Unit,
)

public abstract fun <R> ((T) -> R?).ifPresent(
name: String,
init: ValidationBuilder<R>.() -> Unit,
)

public abstract fun <R> ((T) -> R?).required(
name: String,
init: ValidationBuilder<R>.() -> Unit,
)

public operator fun <R> KProperty1<T, R>.invoke(init: ValidationBuilder<R>.() -> Unit): Unit = invoke(name, init)

public operator fun <R> KFunction1<T, R>.invoke(init: ValidationBuilder<R>.() -> Unit): Unit = invoke("$name()", init)

@JvmName("onEachIterable")
public infix fun <R> KProperty1<T, Iterable<R>>.onEach(init: ValidationBuilder<R>.() -> Unit): Unit = onEachIterable(name, this, init)

@JvmName("onEachIterable")
public infix fun <R> KFunction1<T, Iterable<R>>.onEach(init: ValidationBuilder<R>.() -> Unit): Unit =
onEachIterable("$name()", this, init)

@JvmName("onEachArray")
public infix fun <R> KProperty1<T, Array<R>>.onEach(init: ValidationBuilder<R>.() -> Unit): Unit = onEachArray(name, this, init)

@JvmName("onEachArray")
public infix fun <R> KFunction1<T, Array<R>>.onEach(init: ValidationBuilder<R>.() -> Unit): Unit = onEachArray("$name()", this, init)

@JvmName("onEachMap")
public infix fun <K, V> KProperty1<T, Map<K, V>>.onEach(init: ValidationBuilder<Map.Entry<K, V>>.() -> Unit): Unit =
onEachMap(this, init)
onEachMap(name, this, init)

@JvmName("onEachMap")
public infix fun <K, V> KFunction1<T, Map<K, V>>.onEach(init: ValidationBuilder<Map.Entry<K, V>>.() -> Unit): Unit =
onEachMap("$name()", this, init)

public abstract infix fun <R> KProperty1<T, R?>.ifPresent(init: ValidationBuilder<R>.() -> Unit)
public infix fun <R> KProperty1<T, R?>.ifPresent(init: ValidationBuilder<R>.() -> Unit): Unit = ifPresent(name, init)

public abstract infix fun <R> KProperty1<T, R?>.required(init: ValidationBuilder<R>.() -> Unit)
public infix fun <R> KFunction1<T, R?>.ifPresent(init: ValidationBuilder<R>.() -> Unit): Unit = ifPresent("$name()", init)

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

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

public abstract fun run(validation: Validation<T>)

public abstract val <R> KProperty1<T, R>.has: ValidationBuilder<R>
public abstract val <R> KFunction1<T, R>.has: ValidationBuilder<R>
}

public fun <T : Any> ValidationBuilder<T?>.ifPresent(init: ValidationBuilder<T>.() -> Unit) {
Expand All @@ -84,8 +121,7 @@ public fun <S, T : Iterable<S>> ValidationBuilder<T>.onEach(init: ValidationBuil
public fun <T> ValidationBuilder<Array<T>>.onEach(init: ValidationBuilder<T>.() -> Unit) {
val builder = ValidationBuilderImpl<T>()
init(builder)
@Suppress("UNCHECKED_CAST")
run(ArrayValidation(builder.build()) as Validation<Array<T>>)
run(ArrayValidation(builder.build()))
}

@JvmName("onEachMap")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.konform.validation

import kotlin.reflect.KFunction1
import kotlin.reflect.KProperty1

public interface ValidationError {
Expand Down Expand Up @@ -50,6 +51,7 @@ public data class Invalid(
private fun toPathSegment(it: Any): String {
return when (it) {
is KProperty1<*, *> -> ".${it.name}"
is KFunction1<*, *> -> ".${it.name}()"
is Int -> "[$it]"
else -> ".$it"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import io.konform.validation.Invalid
import io.konform.validation.Valid
import io.konform.validation.Validation
import io.konform.validation.ValidationResult
import kotlin.reflect.KProperty1

internal class OptionalValidation<T : Any>(
private val validation: Validation<T>,
Expand All @@ -28,34 +27,37 @@ internal class RequiredValidation<T : Any>(
}

internal class NonNullPropertyValidation<T, R>(
private val property: KProperty1<T, R>,
val property: (T) -> R,
val name: String,
private val validation: Validation<R>,
) : Validation<T> {
override fun validate(value: T): ValidationResult<T> {
val propertyValue = property(value)
return validation(propertyValue).mapError { ".${property.name}$it" }.map { value }
return validation(propertyValue).mapError { ".${name}$it" }.map { value }
}
}

internal class OptionalPropertyValidation<T, R>(
private val property: KProperty1<T, R?>,
val property: (T) -> R?,
val name: String,
private val validation: Validation<R>,
) : Validation<T> {
override fun validate(value: T): ValidationResult<T> {
val propertyValue = property(value) ?: return Valid(value)
return validation(propertyValue).mapError { ".${property.name}$it" }.map { value }
return validation(propertyValue).mapError { ".${name}$it" }.map { value }
}
}

internal class RequiredPropertyValidation<T, R>(
private val property: KProperty1<T, R?>,
val property: (T) -> R?,
val name: String,
private val validation: Validation<R>,
) : Validation<T> {
override fun validate(value: T): ValidationResult<T> {
val propertyValue =
property(value)
?: return Invalid(mapOf(".${property.name}" to listOf("is required")))
return validation(propertyValue).mapError { ".${property.name}$it" }.map { value }
?: return Invalid(mapOf(".$name" to listOf("is required")))
return validation(propertyValue).mapError { ".${name}$it" }.map { value }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import io.konform.validation.internal.ValidationBuilderImpl.Companion.PropModifi
import io.konform.validation.internal.ValidationBuilderImpl.Companion.PropModifier.Optional
import io.konform.validation.internal.ValidationBuilderImpl.Companion.PropModifier.OptionalRequired
import kotlin.collections.Map.Entry
import kotlin.reflect.KFunction1
import kotlin.reflect.KProperty1

internal class ValidationBuilderImpl<T> : ValidationBuilder<T>() {
Expand All @@ -22,63 +23,65 @@ internal class ValidationBuilderImpl<T> : ValidationBuilder<T>() {
}

private data class SingleValuePropKey<T, R>(
val property: KProperty1<T, R>,
val property: (T) -> R,
val name: String,
val modifier: PropModifier,
) : PropKey<T>() {
override fun build(builder: ValidationBuilderImpl<*>): Validation<T> {
@Suppress("UNCHECKED_CAST")
val validations = (builder as ValidationBuilderImpl<R>).build()
return when (modifier) {
NonNull -> NonNullPropertyValidation(property, validations)
Optional -> OptionalPropertyValidation(property, validations)
OptionalRequired -> RequiredPropertyValidation(property, validations)
NonNull -> NonNullPropertyValidation(property, name, validations)
Optional -> OptionalPropertyValidation(property, name, validations)
OptionalRequired -> RequiredPropertyValidation(property, name, validations)
}
}
}

private data class IterablePropKey<T, R>(
val property: KProperty1<T, Iterable<R>>,
val property: (T) -> Iterable<R>,
val name: String,
val modifier: PropModifier,
) : PropKey<T>() {
override fun build(builder: ValidationBuilderImpl<*>): Validation<T> {
@Suppress("UNCHECKED_CAST")
val validations = (builder as ValidationBuilderImpl<R>).build()
@Suppress("UNCHECKED_CAST")
return when (modifier) {
NonNull -> NonNullPropertyValidation(property, IterableValidation(validations))
Optional -> OptionalPropertyValidation(property, IterableValidation(validations))
OptionalRequired -> RequiredPropertyValidation(property, IterableValidation(validations))
NonNull -> NonNullPropertyValidation(property, name, IterableValidation(validations))
Optional -> OptionalPropertyValidation(property, name, IterableValidation(validations))
OptionalRequired -> RequiredPropertyValidation(property, name, IterableValidation(validations))
}
}
}

private data class ArrayPropKey<T, R>(
val property: KProperty1<T, Array<R>>,
val property: (T) -> Array<R>,
val name: String,
val modifier: PropModifier,
) : PropKey<T>() {
override fun build(builder: ValidationBuilderImpl<*>): Validation<T> {
@Suppress("UNCHECKED_CAST")
val validations = (builder as ValidationBuilderImpl<R>).build()
@Suppress("UNCHECKED_CAST")
return when (modifier) {
NonNull -> NonNullPropertyValidation(property, ArrayValidation(validations))
Optional -> OptionalPropertyValidation(property, ArrayValidation(validations))
OptionalRequired -> RequiredPropertyValidation(property, ArrayValidation(validations))
NonNull -> NonNullPropertyValidation(property, name, ArrayValidation(validations))
Optional -> OptionalPropertyValidation(property, name, ArrayValidation(validations))
OptionalRequired -> RequiredPropertyValidation(property, name, ArrayValidation(validations))
}
}
}

private data class MapPropKey<T, K, V>(
val property: KProperty1<T, Map<K, V>>,
val property: (T) -> Map<K, V>,
val name: String,
val modifier: PropModifier,
) : PropKey<T>() {
override fun build(builder: ValidationBuilderImpl<*>): Validation<T> {
@Suppress("UNCHECKED_CAST")
val validations = (builder as ValidationBuilderImpl<Map.Entry<K, V>>).build()
return when (modifier) {
NonNull -> NonNullPropertyValidation(property, MapValidation(validations))
Optional -> OptionalPropertyValidation(property, MapValidation(validations))
OptionalRequired -> RequiredPropertyValidation(property, MapValidation(validations))
NonNull -> NonNullPropertyValidation(property, name, MapValidation(validations))
Optional -> OptionalPropertyValidation(property, name, MapValidation(validations))
OptionalRequired -> RequiredPropertyValidation(property, name, MapValidation(validations))
}
}
}
Expand All @@ -102,14 +105,20 @@ internal class ValidationBuilderImpl<T> : ValidationBuilder<T>() {
return Constraint(errorMessage, templateValues.toList(), test).also { constraints.add(it) }
}

private fun <R> KProperty1<T, R?>.getOrCreateBuilder(modifier: PropModifier): ValidationBuilder<R> {
val key = SingleValuePropKey(this, modifier)
private fun <R> ((T) -> R?).getOrCreateBuilder(
name: String,
modifier: PropModifier,
): ValidationBuilder<R> {
val key = SingleValuePropKey(this, name, modifier)
@Suppress("UNCHECKED_CAST")
return (subValidations.getOrPut(key, { ValidationBuilderImpl<R>() }) as ValidationBuilder<R>)
}

private fun <R> KProperty1<T, Iterable<R>>.getOrCreateIterablePropertyBuilder(modifier: PropModifier): ValidationBuilder<R> {
val key = IterablePropKey(this, modifier)
private fun <R> ((T) -> Iterable<R>).getOrCreateIterablePropertyBuilder(
name: String,
modifier: PropModifier,
): ValidationBuilder<R> {
val key = IterablePropKey(this, name, modifier)
@Suppress("UNCHECKED_CAST")
return (subValidations.getOrPut(key, { ValidationBuilderImpl<R>() }) as ValidationBuilder<R>)
}
Expand All @@ -119,41 +128,55 @@ internal class ValidationBuilderImpl<T> : ValidationBuilder<T>() {
return (subValidations.getOrPut(this, { ValidationBuilderImpl<R>() }) as ValidationBuilder<R>)
}

override fun <R> KProperty1<T, R>.invoke(init: ValidationBuilder<R>.() -> Unit) {
getOrCreateBuilder(NonNull).also(init)
override fun <R> ((T) -> R).invoke(
name: String,
init: ValidationBuilder<R>.() -> Unit,
) {
getOrCreateBuilder(name, NonNull).also(init)
}

override fun <R> onEachIterable(
prop: KProperty1<T, Iterable<R>>,
name: String,
prop: (T) -> Iterable<R>,
init: ValidationBuilder<R>.() -> Unit,
) {
prop.getOrCreateIterablePropertyBuilder(NonNull).also(init)
prop.getOrCreateIterablePropertyBuilder(name, NonNull).also(init)
}

override fun <R> onEachArray(
prop: KProperty1<T, Array<R>>,
name: String,
prop: (T) -> Array<R>,
init: ValidationBuilder<R>.() -> Unit,
) {
ArrayPropKey(prop, NonNull).getOrCreateBuilder<R>().also(init)
ArrayPropKey(prop, name, NonNull).getOrCreateBuilder<R>().also(init)
}

override fun <K, V> onEachMap(
prop: KProperty1<T, Map<K, V>>,
name: String,
prop: (T) -> Map<K, V>,
init: ValidationBuilder<Entry<K, V>>.() -> Unit,
) {
MapPropKey(prop, NonNull).getOrCreateBuilder<Map.Entry<K, V>>().also(init)
MapPropKey(prop, name, NonNull).getOrCreateBuilder<Map.Entry<K, V>>().also(init)
}

override fun <R> KProperty1<T, R?>.ifPresent(init: ValidationBuilder<R>.() -> Unit) {
getOrCreateBuilder(Optional).also(init)
override fun <R> ((T) -> R?).ifPresent(
name: String,
init: ValidationBuilder<R>.() -> Unit,
) {
getOrCreateBuilder(name, Optional).also(init)
}

override fun <R> KProperty1<T, R?>.required(init: ValidationBuilder<R>.() -> Unit) {
getOrCreateBuilder(OptionalRequired).also(init)
override fun <R> ((T) -> R?).required(
name: String,
init: ValidationBuilder<R>.() -> Unit,
) {
getOrCreateBuilder(name, OptionalRequired).also(init)
}

override val <R> KProperty1<T, R>.has: ValidationBuilder<R>
get() = getOrCreateBuilder(NonNull)
get() = getOrCreateBuilder(name, NonNull)
override val <R> KFunction1<T, R>.has: ValidationBuilder<R>
get() = getOrCreateBuilder(name, NonNull)

override fun run(validation: Validation<T>) {
prebuiltValidations.add(validation)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@ fun <T> countFieldsWithErrors(validationResult: ValidationResult<T>) = (validati
fun countErrors(
validationResult: ValidationResult<*>,
vararg properties: Any,
) = validationResult.get(*properties)?.size
?: 0
) = validationResult.get(*properties)?.size ?: 0
Loading