Skip to content

Commit

Permalink
Support altering Constraint path and adding userContext (#167)
Browse files Browse the repository at this point in the history
  • Loading branch information
dhoepelman authored Nov 13, 2024
1 parent 8ef4325 commit 6cff5e6
Show file tree
Hide file tree
Showing 12 changed files with 284 additions and 58 deletions.
29 changes: 27 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,39 @@ val validateUser = Validation<UserProfile> {
}
```

#### Custom context

You can add customs context to validation errors

```kotlin
val validateUser = Validation<UserProfile> {
UserProfile::age {
minimum(0) userContext Severity.ERROR
// You can also set multiple things at once
minimum(0).replace(
hint = "Registering before birth is not supported",
userContext = Severity.ERROR,
)
}
}
```

#### Custom validations

You can add custom validations on properties by using `addConstraint`
You can add custom validations on properties by using `constrain`

```kotlin
val validateUser = Validation<UserProfile> {
UserProfile::fullName {
addConstraint("Name cannot contain a tab") { !it.contains("\t") }
constrain("Name cannot contain a tab") { !it.contains("\t") }
// Set a custom path for the error
constrain("Name must have a non-whitespace character", path = ValidationPath.of("trimmedName")) {
it.trim().isNotEmpty()
}
// Set custom context
constrain("Must have 5 characters", userContext = Severity.ERROR) {
it.size >= 5
}
}
}
```
Expand Down
46 changes: 41 additions & 5 deletions api/konform.api
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
public final class io/konform/validation/Constraint {
public static final field Companion Lio/konform/validation/Constraint$Companion;
public static final field VALUE_IN_HINT Ljava/lang/String;
public fun <init> (Ljava/lang/String;Lio/konform/validation/path/ValidationPath;Ljava/lang/Object;Ljava/util/List;Lkotlin/jvm/functions/Function1;)V
public synthetic fun <init> (Ljava/lang/String;Lio/konform/validation/path/ValidationPath;Ljava/lang/Object;Ljava/util/List;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Lio/konform/validation/path/ValidationPath;
public final fun component3 ()Ljava/lang/Object;
public final fun component4 ()Ljava/util/List;
public final fun component5 ()Lkotlin/jvm/functions/Function1;
public final fun copy (Ljava/lang/String;Lio/konform/validation/path/ValidationPath;Ljava/lang/Object;Ljava/util/List;Lkotlin/jvm/functions/Function1;)Lio/konform/validation/Constraint;
public static synthetic fun copy$default (Lio/konform/validation/Constraint;Ljava/lang/String;Lio/konform/validation/path/ValidationPath;Ljava/lang/Object;Ljava/util/List;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/konform/validation/Constraint;
public fun equals (Ljava/lang/Object;)Z
public final fun getHint ()Ljava/lang/String;
public final fun getPath ()Lio/konform/validation/path/ValidationPath;
public final fun getTemplateValues ()Ljava/util/List;
public final fun getTest ()Lkotlin/jvm/functions/Function1;
public final fun getUserContext ()Ljava/lang/Object;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

Expand Down Expand Up @@ -64,7 +77,10 @@ public final class io/konform/validation/ValidationBuilder {
public static final field Companion Lio/konform/validation/ValidationBuilder$Companion;
public fun <init> ()V
public final fun addConstraint (Ljava/lang/String;[Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lio/konform/validation/Constraint;
public final fun applyConstraint (Lio/konform/validation/Constraint;)Lio/konform/validation/Constraint;
public final fun build ()Lio/konform/validation/Validation;
public final fun constrain (Ljava/lang/String;Lio/konform/validation/path/ValidationPath;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Lio/konform/validation/Constraint;
public static synthetic fun constrain$default (Lio/konform/validation/ValidationBuilder;Ljava/lang/String;Lio/konform/validation/path/ValidationPath;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/konform/validation/Constraint;
public final fun hint (Lio/konform/validation/Constraint;Ljava/lang/String;)Lio/konform/validation/Constraint;
public final fun ifPresent (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
public final fun ifPresent (Lkotlin/reflect/KFunction;Lkotlin/jvm/functions/Function1;)V
Expand All @@ -77,10 +93,15 @@ public final class io/konform/validation/ValidationBuilder {
public final fun onEachIterable (Lkotlin/reflect/KProperty1;Lkotlin/jvm/functions/Function1;)V
public final fun onEachMap (Lkotlin/reflect/KFunction;Lkotlin/jvm/functions/Function1;)V
public final fun onEachMap (Lkotlin/reflect/KProperty1;Lkotlin/jvm/functions/Function1;)V
public final fun path (Lio/konform/validation/Constraint;Lio/konform/validation/path/ValidationPath;)Lio/konform/validation/Constraint;
public final fun replace (Lio/konform/validation/Constraint;Ljava/lang/String;Lio/konform/validation/path/ValidationPath;Ljava/lang/Object;)Lio/konform/validation/Constraint;
public static synthetic fun replace$default (Lio/konform/validation/ValidationBuilder;Lio/konform/validation/Constraint;Ljava/lang/String;Lio/konform/validation/path/ValidationPath;Ljava/lang/Object;ILjava/lang/Object;)Lio/konform/validation/Constraint;
public final fun replaceConstraint (Lio/konform/validation/Constraint;Lio/konform/validation/Constraint;)Lio/konform/validation/Constraint;
public final fun required (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
public final fun required (Lkotlin/reflect/KFunction;Lkotlin/jvm/functions/Function1;)V
public final fun required (Lkotlin/reflect/KProperty1;Lkotlin/jvm/functions/Function1;)V
public final fun run (Lio/konform/validation/Validation;)V
public final fun userContext (Lio/konform/validation/Constraint;Ljava/lang/Object;)Lio/konform/validation/Constraint;
public final fun validate (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
}

Expand All @@ -97,15 +118,18 @@ public final class io/konform/validation/ValidationBuilderKt {
}

public final class io/konform/validation/ValidationError {
public fun <init> (Lio/konform/validation/path/ValidationPath;Ljava/lang/String;)V
public fun <init> (Lio/konform/validation/path/ValidationPath;Ljava/lang/String;Ljava/lang/Object;)V
public synthetic fun <init> (Lio/konform/validation/path/ValidationPath;Ljava/lang/String;Ljava/lang/Object;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lio/konform/validation/path/ValidationPath;
public final fun component2 ()Ljava/lang/String;
public final fun copy (Lio/konform/validation/path/ValidationPath;Ljava/lang/String;)Lio/konform/validation/ValidationError;
public static synthetic fun copy$default (Lio/konform/validation/ValidationError;Lio/konform/validation/path/ValidationPath;Ljava/lang/String;ILjava/lang/Object;)Lio/konform/validation/ValidationError;
public final fun component3 ()Ljava/lang/Object;
public final fun copy (Lio/konform/validation/path/ValidationPath;Ljava/lang/String;Ljava/lang/Object;)Lio/konform/validation/ValidationError;
public static synthetic fun copy$default (Lio/konform/validation/ValidationError;Lio/konform/validation/path/ValidationPath;Ljava/lang/String;Ljava/lang/Object;ILjava/lang/Object;)Lio/konform/validation/ValidationError;
public fun equals (Ljava/lang/Object;)Z
public final fun getDataPath ()Ljava/lang/String;
public final fun getMessage ()Ljava/lang/String;
public final fun getPath ()Lio/konform/validation/path/ValidationPath;
public final fun getUserContext ()Ljava/lang/Object;
public fun hashCode ()I
public final fun mapPath (Lkotlin/jvm/functions/Function1;)Lio/konform/validation/ValidationError;
public fun toString ()Ljava/lang/String;
Expand Down Expand Up @@ -311,8 +335,7 @@ public final class io/konform/validation/path/ValidationPath {
}

public final class io/konform/validation/path/ValidationPath$Companion {
public final fun fromAny ([Ljava/lang/Object;)Lio/konform/validation/path/ValidationPath;
public final fun of (Lio/konform/validation/path/PathSegment;)Lio/konform/validation/path/ValidationPath;
public final fun of ([Ljava/lang/Object;)Lio/konform/validation/path/ValidationPath;
}

public final class io/konform/validation/types/CallableValidation : io/konform/validation/Validation {
Expand All @@ -325,6 +348,19 @@ public final class io/konform/validation/types/CallableValidation : io/konform/v
public fun validate (Ljava/lang/Object;)Lio/konform/validation/ValidationResult;
}

public final class io/konform/validation/types/ConstraintsValidation : io/konform/validation/Validation {
public fun <init> (Ljava/util/List;)V
public final fun copy (Ljava/util/List;)Lio/konform/validation/types/ConstraintsValidation;
public static synthetic fun copy$default (Lio/konform/validation/types/ConstraintsValidation;Ljava/util/List;ILjava/lang/Object;)Lio/konform/validation/types/ConstraintsValidation;
public fun equals (Ljava/lang/Object;)Z
public fun hashCode ()I
public fun invoke (Ljava/lang/Object;)Lio/konform/validation/ValidationResult;
public fun prependPath (Lio/konform/validation/path/PathSegment;)Lio/konform/validation/Validation;
public fun prependPath (Lio/konform/validation/path/ValidationPath;)Lio/konform/validation/Validation;
public fun toString ()Ljava/lang/String;
public fun validate (Ljava/lang/Object;)Lio/konform/validation/ValidationResult;
}

public final class io/konform/validation/types/EmptyValidation : io/konform/validation/Validation {
public static final field INSTANCE Lio/konform/validation/types/EmptyValidation;
public fun equals (Ljava/lang/Object;)Z
Expand Down
12 changes: 10 additions & 2 deletions src/commonMain/kotlin/io/konform/validation/Constraint.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
package io.konform.validation

import io.konform.validation.path.ValidationPath

/**
* @param hint for a failed validation. "{value}" will be replaced by the toString-ed value that is being validated
* @param path [ValidationPath] for this constraint, will be reported in [ValidationError]
* @param userContext Optional context set by the user to add more information to the validation, e.g. severity
* @param test the predicate that must be satisfied
* @see [ValidationBuilder.hint]
* @see [ValidationBuilder.path]
* @see [ValidationBuilder.userContext]
*/
public class Constraint<in R> internal constructor(
public data class Constraint<in R>(
public val hint: String,
public val path: ValidationPath = ValidationPath.EMPTY,
public val userContext: Any? = null,
@Deprecated("Put template parameters directly into the hint.")
public val templateValues: List<String> = emptyList(),
public val test: (R) -> Boolean,
// TODO: Add customizable Path parameter settable with a path() method
) {
override fun toString(): String = "Constraint(\"$hint\")"

Expand Down
58 changes: 49 additions & 9 deletions src/commonMain/kotlin/io/konform/validation/ValidationBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,46 @@ public class ValidationBuilder<T> {
subValidations
.let {
if (constraints.isNotEmpty()) {
it.prepend(ConstraintsValidation(ValidationPath.EMPTY, constraints))
it.prepend(ConstraintsValidation(constraints))
} else {
it
}
}.flatten()

@Deprecated(
"Use constrain(), templateValues are no longer supported, put them directly in the hint",
ReplaceWith("constrain(errorMessage, test)"),
)
public fun addConstraint(
errorMessage: String,
vararg templateValues: String,
test: (T) -> Boolean,
): Constraint<T> = Constraint(errorMessage, templateValues.toList(), test).also { constraints.add(it) }
): Constraint<T> = applyConstraint(Constraint(errorMessage, templateValues = templateValues.toList(), test = test))

@Suppress("DEPRECATION")
public infix fun Constraint<T>.hint(hint: String): Constraint<T> =
Constraint(hint, this.templateValues, this.test).also {
constraints.remove(this)
constraints.add(it)
}
/** Add a new [Constraint] to this validation. */
public fun constrain(
hint: String,
path: ValidationPath = ValidationPath.EMPTY,
userContext: Any? = null,
test: (T) -> Boolean,
): Constraint<T> = applyConstraint(Constraint(hint, path, userContext, emptyList(), test))

/** Replace one or more properties of a [Constraint]. */
public fun Constraint<T>.replace(
hint: String = this.hint,
path: ValidationPath = this.path,
userContext: Any? = this.userContext,
): Constraint<T> = replaceConstraint(this, this.copy(hint = hint, path = path, userContext = userContext))

/** Change the hint on a [Constraint]. */
public infix fun Constraint<T>.hint(hint: String): Constraint<T> = replaceConstraint(this, this.copy(hint = hint))

/** Change the path on a [Constraint]. */
public infix fun Constraint<T>.path(path: ValidationPath): Constraint<T> = replaceConstraint(this, this.copy(path = path))

/** Change the userContext on a [Constraint]. */
public infix fun Constraint<T>.userContext(userContext: Any?): Constraint<T> =
replaceConstraint(this, this.copy(userContext = userContext))

private fun <R> onEachIterable(
pathSegment: PathSegment,
Expand Down Expand Up @@ -90,7 +112,7 @@ public class ValidationBuilder<T> {
public infix fun <K, V> KFunction1<T, Map<K, V>>.onEach(init: ValidationBuilder<Map.Entry<K, V>>.() -> Unit): Unit =
onEachMap(FuncRef(this), this, init)

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

public operator fun <R> KFunction1<T, R>.invoke(init: ValidationBuilder<R>.() -> Unit): Unit = validate(this, this, init)

Expand Down Expand Up @@ -136,6 +158,24 @@ public class ValidationBuilder<T> {
subValidations.add(validation)
}

/** Add a [Constraint] and return it. */
public fun applyConstraint(constraint: Constraint<T>): Constraint<T> {
constraints.add(constraint)
return constraint
}

/** Replace a [Constraint] and return the replacement */
public fun replaceConstraint(
old: Constraint<T>,
replacement: Constraint<T>,
): Constraint<T> {
// It's very likely that the last added constraint is the one to be replaced so optimize for that
val idx = if (constraints.lastOrNull() === old) constraints.size - 1 else constraints.indexOf(old)
if (idx == -1) throw IllegalArgumentException("Not found in existing constraints: $old")
constraints[idx] = replacement
return replacement
}

public inline fun <reified SubT : T & Any> ifInstanceOf(init: ValidationBuilder<SubT>.() -> Unit): Unit =
run(IsClassValidation<SubT, T>(SubT::class, required = false, buildWithNew(init)))

Expand Down
12 changes: 5 additions & 7 deletions src/commonMain/kotlin/io/konform/validation/ValidationError.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import io.konform.validation.path.ValidationPath
public data class ValidationError(
public val path: ValidationPath,
public val message: String,
public val userContext: Any? = null,
) {
public val dataPath: String get() = path.dataPath

Expand All @@ -19,24 +20,21 @@ public data class ValidationError(

internal companion object {
internal fun of(
pathSegment: PathSegment,
pathSegment: Any,
message: String,
): ValidationError = ValidationError(ValidationPath.of(pathSegment), message)

internal fun ofAny(
pathSegment: Any,
message: String,
): ValidationError = of(PathSegment.toPathSegment(pathSegment), message)
internal fun ofEmptyPath(message: String): ValidationError = ValidationError(ValidationPath.EMPTY, message)
}
}

public fun List<ValidationError>.filterPath(vararg validationPath: Any): List<ValidationError> {
val path = ValidationPath.fromAny(*validationPath)
val path = ValidationPath.of(*validationPath)
return filter { it.path == path }
}

public fun List<ValidationError>.filterDataPath(vararg validationPath: Any): List<ValidationError> {
val dataPath = ValidationPath.fromAny(*validationPath).dataPath
val dataPath = ValidationPath.of(*validationPath).dataPath
return filter { it.dataPath == dataPath }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,6 @@ public data class ValidationPath(
public companion object {
internal val EMPTY = ValidationPath(emptyList())

public fun of(pathSegment: PathSegment): ValidationPath = ValidationPath(listOf(pathSegment))

public fun fromAny(vararg validationPath: Any): ValidationPath =
ValidationPath(validationPath.map { PathSegment.toPathSegment(it) })
public fun of(vararg validationPath: Any): ValidationPath = ValidationPath(validationPath.map { PathSegment.toPathSegment(it) })
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,19 @@ import io.konform.validation.Valid
import io.konform.validation.Validation
import io.konform.validation.ValidationError
import io.konform.validation.ValidationResult
import io.konform.validation.path.ValidationPath

internal class ConstraintsValidation<T>(
private val path: ValidationPath = ValidationPath.EMPTY,
public data class ConstraintsValidation<T>(
private val constraints: List<Constraint<T>>,
) : Validation<T> {
override fun validate(value: T): ValidationResult<T> =
constraints
.filterNot { it.test(value) }
.map { ValidationError(path, it.createHint(value)) }
.map { ValidationError(it.path, it.createHint(value), userContext = it.userContext) }
.let { errors ->
if (errors.isEmpty()) {
Valid(value)
} else {
Invalid(errors)
}
}

override fun toString(): String = "ConstraintsValidation(path=$path,constraints=$constraints)"
}
Loading

0 comments on commit 6cff5e6

Please sign in to comment.