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

Enable explicit API mode #90

Merged
merged 1 commit into from
Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ group = projectGroup
version = projectVersion

kotlin {
// Since we are a library, prevent accidentally making things part of the public API
explicitApi()

sourceSets.all {
languageSettings {
languageVersion = kotlinApiTarget
Expand Down
16 changes: 10 additions & 6 deletions src/commonMain/kotlin/io/konform/validation/Validation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@ package io.konform.validation

import io.konform.validation.internal.ValidationBuilderImpl

interface Validation<T> {
companion object {
operator fun <T> invoke(init: ValidationBuilder<T>.() -> Unit): Validation<T> {
public interface Validation<T> {
public companion object {
public operator fun <T> invoke(init: ValidationBuilder<T>.() -> Unit): Validation<T> {
val builder = ValidationBuilderImpl<T>()
return builder.apply(init).build()
}
}

fun validate(value: T): ValidationResult<T>
public fun validate(value: T): ValidationResult<T>

operator fun invoke(value: T) = validate(value)
public operator fun invoke(value: T): ValidationResult<T> = validate(value)
}

class Constraint<R> internal constructor(val hint: String, val templateValues: List<String>, val test: (R) -> Boolean)
public class Constraint<R> internal constructor(
public val hint: String,
public val templateValues: List<String>,
public val test: (R) -> Boolean,
)
35 changes: 18 additions & 17 deletions src/commonMain/kotlin/io/konform/validation/ValidationBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,82 +13,83 @@ import kotlin.reflect.KProperty1
private annotation class ValidationScope

@ValidationScope
abstract class ValidationBuilder<T> {
abstract fun build(): Validation<T>
public abstract class ValidationBuilder<T> {
public abstract fun build(): Validation<T>

abstract fun addConstraint(
public abstract fun addConstraint(
errorMessage: String,
vararg templateValues: String,
test: (T) -> Boolean,
): Constraint<T>

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

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

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

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

@JvmName("onEachArray")
infix fun <R> KProperty1<T, Array<R>>.onEach(init: ValidationBuilder<R>.() -> Unit) = onEachArray(this, init)
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>>,
init: ValidationBuilder<Map.Entry<K, V>>.() -> Unit,
)

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

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

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

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

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

fun <T : Any> ValidationBuilder<T?>.ifPresent(init: ValidationBuilder<T>.() -> Unit) {
public fun <T : Any> ValidationBuilder<T?>.ifPresent(init: ValidationBuilder<T>.() -> Unit) {
val builder = ValidationBuilderImpl<T>()
init(builder)
run(OptionalValidation(builder.build()))
}

fun <T : Any> ValidationBuilder<T?>.required(init: ValidationBuilder<T>.() -> Unit) {
public fun <T : Any> ValidationBuilder<T?>.required(init: ValidationBuilder<T>.() -> Unit) {
val builder = ValidationBuilderImpl<T>()
init(builder)
run(RequiredValidation(builder.build()))
}

@JvmName("onEachIterable")
fun <S, T : Iterable<S>> ValidationBuilder<T>.onEach(init: ValidationBuilder<S>.() -> Unit) {
public fun <S, T : Iterable<S>> ValidationBuilder<T>.onEach(init: ValidationBuilder<S>.() -> Unit) {
val builder = ValidationBuilderImpl<S>()
init(builder)
@Suppress("UNCHECKED_CAST")
run(IterableValidation(builder.build()) as Validation<T>)
}

@JvmName("onEachArray")
fun <T> ValidationBuilder<Array<T>>.onEach(init: ValidationBuilder<T>.() -> Unit) {
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>>)
}

@JvmName("onEachMap")
fun <K, V, T : Map<K, V>> ValidationBuilder<T>.onEach(init: ValidationBuilder<Map.Entry<K, V>>.() -> Unit) {
public fun <K, V, T : Map<K, V>> ValidationBuilder<T>.onEach(init: ValidationBuilder<Map.Entry<K, V>>.() -> Unit) {
val builder = ValidationBuilderImpl<Map.Entry<K, V>>()
init(builder)
@Suppress("UNCHECKED_CAST")
Expand Down
20 changes: 10 additions & 10 deletions src/commonMain/kotlin/io/konform/validation/ValidationResult.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package io.konform.validation

import kotlin.reflect.KProperty1

interface ValidationError {
val dataPath: String
val message: String
public interface ValidationError {
public val dataPath: String
public val message: String
}

internal data class PropertyValidationError(
Expand All @@ -16,7 +16,7 @@ internal data class PropertyValidationError(
}
}

interface ValidationErrors : List<ValidationError>
public interface ValidationErrors : List<ValidationError>

internal object NoValidationErrors : ValidationErrors, List<ValidationError> by emptyList()

Expand All @@ -26,15 +26,15 @@ internal class DefaultValidationErrors(private val errors: List<ValidationError>
}
}

sealed class ValidationResult<out T> {
abstract operator fun get(vararg propertyPath: Any): List<String>?
public sealed class ValidationResult<out T> {
public abstract operator fun get(vararg propertyPath: Any): List<String>?

abstract fun <R> map(transform: (T) -> R): ValidationResult<R>
public abstract fun <R> map(transform: (T) -> R): ValidationResult<R>

abstract val errors: ValidationErrors
public abstract val errors: ValidationErrors
}

data class Invalid<T>(
public data class Invalid<T>(
internal val internalErrors: Map<String, List<String>>,
) : ValidationResult<T>() {
override fun get(vararg propertyPath: Any): List<String>? = internalErrors[propertyPath.joinToString("", transform = ::toPathSegment)]
Expand Down Expand Up @@ -62,7 +62,7 @@ data class Invalid<T>(
}
}

data class Valid<T>(val value: T) : ValidationResult<T>() {
public data class Valid<T>(val value: T) : ValidationResult<T>() {
override fun get(vararg propertyPath: Any): List<String>? = null

override fun <R> map(transform: (T) -> R): ValidationResult<R> = Valid(transform(this.value))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,32 @@ import io.konform.validation.Constraint
import io.konform.validation.ValidationBuilder
import kotlin.math.roundToInt

inline fun <reified T> ValidationBuilder<*>.type() =
public inline fun <reified T> ValidationBuilder<*>.type(): Constraint<*> =
addConstraint(
"must be of the correct type",
) { it is T }

fun <T> ValidationBuilder<T>.enum(vararg allowed: T) =
public fun <T> ValidationBuilder<T>.enum(vararg allowed: T): Constraint<T> =
addConstraint(
"must be one of: {0}",
allowed.joinToString("', '", "'", "'"),
) { it in allowed }

inline fun <reified T : Enum<T>> ValidationBuilder<String>.enum(): Constraint<String> {
public inline fun <reified T : Enum<T>> ValidationBuilder<String>.enum(): Constraint<String> {
val enumNames = enumValues<T>().map { it.name }
return addConstraint(
"must be one of: {0}",
enumNames.joinToString("', '", "'", "'"),
) { it in enumNames }
}

fun <T> ValidationBuilder<T>.const(expected: T) =
public fun <T> ValidationBuilder<T>.const(expected: T): Constraint<T> =
addConstraint(
"must be {0}",
expected?.let { "'$it'" } ?: "null",
) { expected == it }

fun <T : Number> ValidationBuilder<T>.multipleOf(factor: Number): Constraint<T> {
public fun <T : Number> ValidationBuilder<T>.multipleOf(factor: Number): Constraint<T> {
val factorAsDouble = factor.toDouble()
require(factorAsDouble > 0) { "multipleOf requires the factor to be strictly larger than 0" }
return addConstraint("must be a multiple of '{0}'", factor.toString()) {
Expand All @@ -38,55 +38,55 @@ fun <T : Number> ValidationBuilder<T>.multipleOf(factor: Number): Constraint<T>
}
}

fun <T : Number> ValidationBuilder<T>.maximum(maximumInclusive: Number) =
public fun <T : Number> ValidationBuilder<T>.maximum(maximumInclusive: Number): Constraint<T> =
addConstraint(
"must be at most '{0}'",
maximumInclusive.toString(),
) { it.toDouble() <= maximumInclusive.toDouble() }

fun <T : Number> ValidationBuilder<T>.exclusiveMaximum(maximumExclusive: Number) =
public fun <T : Number> ValidationBuilder<T>.exclusiveMaximum(maximumExclusive: Number): Constraint<T> =
addConstraint(
"must be less than '{0}'",
maximumExclusive.toString(),
) { it.toDouble() < maximumExclusive.toDouble() }

fun <T : Number> ValidationBuilder<T>.minimum(minimumInclusive: Number) =
public fun <T : Number> ValidationBuilder<T>.minimum(minimumInclusive: Number): Constraint<T> =
addConstraint(
"must be at least '{0}'",
minimumInclusive.toString(),
) { it.toDouble() >= minimumInclusive.toDouble() }

fun <T : Number> ValidationBuilder<T>.exclusiveMinimum(minimumExclusive: Number) =
public fun <T : Number> ValidationBuilder<T>.exclusiveMinimum(minimumExclusive: Number): Constraint<T> =
addConstraint(
"must be greater than '{0}'",
minimumExclusive.toString(),
) { it.toDouble() > minimumExclusive.toDouble() }

fun ValidationBuilder<String>.minLength(length: Int): Constraint<String> {
public fun ValidationBuilder<String>.minLength(length: Int): Constraint<String> {
require(length >= 0) { IllegalArgumentException("minLength requires the length to be >= 0") }
return addConstraint(
"must have at least {0} characters",
length.toString(),
) { it.length >= length }
}

fun ValidationBuilder<String>.maxLength(length: Int): Constraint<String> {
public fun ValidationBuilder<String>.maxLength(length: Int): Constraint<String> {
require(length >= 0) { IllegalArgumentException("maxLength requires the length to be >= 0") }
return addConstraint(
"must have at most {0} characters",
length.toString(),
) { it.length <= length }
}

fun ValidationBuilder<String>.pattern(pattern: String) = pattern(pattern.toRegex())
public fun ValidationBuilder<String>.pattern(pattern: String): Constraint<String> = pattern(pattern.toRegex())

fun ValidationBuilder<String>.pattern(pattern: Regex) =
public fun ValidationBuilder<String>.pattern(pattern: Regex): Constraint<String> =
addConstraint(
"must match the expected pattern",
pattern.toString(),
) { it.matches(pattern) }

inline fun <reified T> ValidationBuilder<T>.minItems(minSize: Int): Constraint<T> =
public inline fun <reified T> ValidationBuilder<T>.minItems(minSize: Int): Constraint<T> =
addConstraint(
"must have at least {0} items",
minSize.toString(),
Expand All @@ -99,7 +99,7 @@ inline fun <reified T> ValidationBuilder<T>.minItems(minSize: Int): Constraint<T
}
}

inline fun <reified T> ValidationBuilder<T>.maxItems(maxSize: Int): Constraint<T> =
public inline fun <reified T> ValidationBuilder<T>.maxItems(maxSize: Int): Constraint<T> =
addConstraint(
"must have at most {0} items",
maxSize.toString(),
Expand All @@ -112,13 +112,13 @@ inline fun <reified T> ValidationBuilder<T>.maxItems(maxSize: Int): Constraint<T
}
}

inline fun <reified T : Map<*, *>> ValidationBuilder<T>.minProperties(minSize: Int): Constraint<T> =
public inline fun <reified T : Map<*, *>> ValidationBuilder<T>.minProperties(minSize: Int): Constraint<T> =
minItems(minSize) hint "must have at least {0} properties"

inline fun <reified T : Map<*, *>> ValidationBuilder<T>.maxProperties(maxSize: Int): Constraint<T> =
public inline fun <reified T : Map<*, *>> ValidationBuilder<T>.maxProperties(maxSize: Int): Constraint<T> =
maxItems(maxSize) hint "must have at most {0} properties"

inline fun <reified T> ValidationBuilder<T>.uniqueItems(unique: Boolean): Constraint<T> =
public inline fun <reified T> ValidationBuilder<T>.uniqueItems(unique: Boolean): Constraint<T> =
addConstraint(
"all items must be unique",
) {
Expand Down