Skip to content

Commit

Permalink
Allow ValidationPath in validate() and ValidationPath.of (#169)
Browse files Browse the repository at this point in the history
  • Loading branch information
dhoepelman authored Nov 14, 2024
1 parent 2310894 commit 6ec40a4
Show file tree
Hide file tree
Showing 9 changed files with 207 additions and 92 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ val validateUser = Validation<UserProfile> {
ifPresent("yourName", /* ... */) {
// your validations, only running if the result is not null
}
// You can use a more extensive path, for example
// the path will be ".fullName.trimmed" here:
validate(ValidationPath.of(UserProfile::fullName, "trimmed"), { /* ... */ }) {
/* ... */
}
}
```

Expand Down
9 changes: 9 additions & 0 deletions api/konform.api
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,15 @@ public final class io/konform/validation/path/ValidationPath$Companion {
public final fun of ([Ljava/lang/Object;)Lio/konform/validation/path/ValidationPath;
}

public final class io/konform/validation/types/AlwaysInvalidValidation : io/konform/validation/Validation {
public static final field INSTANCE Lio/konform/validation/types/AlwaysInvalidValidation;
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 validate (Ljava/lang/Object;)Lio/konform/validation/Invalid;
public synthetic fun validate (Ljava/lang/Object;)Lio/konform/validation/ValidationResult;
}

public final class io/konform/validation/types/CallableValidation : io/konform/validation/Validation {
public fun <init> (Lio/konform/validation/path/ValidationPath;Lkotlin/jvm/functions/Function1;Lio/konform/validation/Validation;)V
public synthetic fun <init> (Lio/konform/validation/path/ValidationPath;Lkotlin/jvm/functions/Function1;Lio/konform/validation/Validation;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
Expand Down
21 changes: 12 additions & 9 deletions src/commonMain/kotlin/io/konform/validation/ValidationBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import io.konform.validation.ValidationBuilder.Companion.buildWithNew
import io.konform.validation.helpers.prepend
import io.konform.validation.path.FuncRef
import io.konform.validation.path.PathSegment
import io.konform.validation.path.PathSegment.Companion.toPathSegment
import io.konform.validation.path.PropRef
import io.konform.validation.path.ValidationPath
import io.konform.validation.types.ArrayValidation
Expand Down Expand Up @@ -126,33 +125,37 @@ public class ValidationBuilder<T> {

/**
* Calculate a value from the input and run a validation on it.
* @param pathSegment The [PathSegment] of the validation.
* is [Any] for backwards compatibility and easy of use, see [toPathSegment]
* @param path The [PathSegment] or [ValidationPath] of the validation.
* is [Any] for backwards compatibility and ease of use, see [ValidationPath.of].
* @param f The function for which you want to validate the result of
*/
public fun <R> validate(
pathSegment: Any,
path: Any,
f: (T) -> R,
init: ValidationBuilder<R>.() -> Unit,
): Unit = run(CallableValidation(pathSegment, f, buildWithNew(init)))
): Unit = run(CallableValidation(path, f, buildWithNew(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.
* is [Any] for backwards compatibility and ease of use, see [ValidationPath.of].
*/
public fun <R> ifPresent(
pathSegment: Any,
path: Any,
f: (T) -> R?,
init: ValidationBuilder<R>.() -> Unit,
): Unit = run(CallableValidation(pathSegment, f, buildWithNew(init).ifPresent()))
): Unit = run(CallableValidation(path, f, buildWithNew(init).ifPresent()))

/**
* Calculate a value from the input and run a validation on it, and give an error if the result is null.
* @param path The [PathSegment] or [ValidationPath] of the validation.
* is [Any] for backwards compatibility and ease of use, see [ValidationPath.of].
*/
public fun <R> required(
pathSegment: Any,
path: Any,
f: (T) -> R?,
init: ValidationBuilder<R>.() -> Unit,
): Unit = run(CallableValidation(pathSegment, f, buildWithNew(init).required()))
): Unit = run(CallableValidation(path, f, buildWithNew(init).required()))

public fun run(validation: Validation<T>) {
subValidations.add(validation)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ public sealed interface PathSegment {
is Int -> PathIndex(pathSegment)
is Map.Entry<*, *> -> pathSegment.toPathSegment()
is KClass<*> -> PathClass(pathSegment)
is ValidationPath -> throw IllegalArgumentException(
"Converting $pathSegment to a PathSegment is likely a mistake, as it would lead to a nested ValidationPath",
)

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

public fun of(vararg validationPath: Any): ValidationPath = ValidationPath(validationPath.map { PathSegment.toPathSegment(it) })
/**
* Convert the specified arguments into a [ValidationPath]
* any [ValidationPath] given will be merged, anything else will be converted to [PathSegment]
* using [PathSegment.toPathSegment].
*/
public fun of(vararg validationPath: Any): ValidationPath =
ValidationPath(
validationPath.flatMap {
if (it is ValidationPath) {
it.segments
} else {
listOf(PathSegment.toPathSegment(it))
}
},
)
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package io.konform.validation.types

import io.konform.validation.Invalid
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

/** Validation that always returns [Valid]. `unit` in monadic terms. */
/** [Validation] that always returns [Valid]. `unit` in monadic terms. */
public object EmptyValidation : Validation<Any?> {
override fun validate(value: Any?): ValidationResult<Any?> = Valid(value)

Expand All @@ -14,3 +17,8 @@ public object EmptyValidation : Validation<Any?> {

override fun hashCode(): Int = 912378
}

/** [Validation] that always returns [Invalid]. */
public object AlwaysInvalidValidation : Validation<Any?> {
override fun validate(value: Any?): Invalid = Invalid(listOf(ValidationError(ValidationPath.EMPTY, "always invalid")))
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import io.konform.validation.Invalid
import io.konform.validation.Valid
import io.konform.validation.Validation
import io.konform.validation.ValidationResult
import io.konform.validation.path.PathSegment
import io.konform.validation.path.ValidationPath

/** Validate the result of a property/function. */
Expand All @@ -13,8 +12,8 @@ public class CallableValidation<T, R>(
private val callable: (T) -> R,
private val validation: Validation<R>,
) : Validation<T> {
internal constructor(pathSegment: Any, callable: (T) -> R, validation: Validation<R>) :
this(ValidationPath.of(PathSegment.toPathSegment(pathSegment)), callable, validation)
internal constructor(path: Any, callable: (T) -> R, validation: Validation<R>) :
this(ValidationPath.of(path), callable, validation)

override fun validate(value: T): ValidationResult<T> {
val toValidate = callable(value)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package io.konform.validation

import io.konform.validation.constraints.const
import io.konform.validation.constraints.containsPattern
import io.konform.validation.constraints.enum
import io.konform.validation.constraints.maxLength
import io.konform.validation.constraints.minItems
import io.konform.validation.constraints.minLength
Expand Down Expand Up @@ -206,82 +204,6 @@ class ValidationBuilderTest {
)
}

@Test
fun validateLambda() {
val splitDoubleValidation =
Validation<Register> {
validate("getPasswordLambda", { r: Register -> r.password }) {
minLength(1)
maxLength(10)
}
validate("getEmailLambda", { r: Register -> r.email }) {
pattern(".+@.+".toRegex()).hint("must have correct format")
}
}

splitDoubleValidation shouldBeValid Register(email = "[email protected]", password = "a")
splitDoubleValidation.shouldBeInvalid(Register(email = "[email protected]", password = "")) {
it.shouldContainOnlyError(ValidationError.of("getPasswordLambda", "must have at least 1 characters"))
}
splitDoubleValidation.shouldBeInvalid(Register(email = "[email protected]", password = "aaaaaaaaaaa")) {
it.shouldContainOnlyError(ValidationError.of("getPasswordLambda", "must have at most 10 characters"))
}
splitDoubleValidation.shouldBeInvalid(Register(email = "tester@", password = "")) {
it.shouldContainExactlyErrors(
ValidationError.of("getPasswordLambda", "must have at least 1 characters"),
ValidationError.of("getEmailLambda", "must have correct format"),
)
}
}

@Test
fun complexLambdaAccessors() {
data class Token(
val claims: Map<String, String>,
)

fun ValidationBuilder<Token>.validateClaim(
key: String,
validations: ValidationBuilder<String>.() -> Unit,
) {
required("claim_$key", { data: Token -> data.claims[key] }) {
validations()
}
}

val accessTokenValidation =
Validation<Token> {
validateClaim("scope") {
const("access")
}
validateClaim("issuer") {
enum("bob", "eve")
}
}
val refreshTokenVerification =
Validation<Token> {
validateClaim("scope") {
const("refresh")
}
validateClaim("issuer") {
enum("bob", "eve")
}
}

Token(mapOf("scope" to "access", "issuer" to "bob")).let {
assertEquals(Valid(it), accessTokenValidation(it))
assertEquals(1, countFieldsWithErrors(refreshTokenVerification(it)))
}
Token(mapOf("scope" to "refresh", "issuer" to "eve")).let {
assertEquals(Valid(it), refreshTokenVerification(it))
assertEquals(1, countFieldsWithErrors(accessTokenValidation(it)))
}
Token(mapOf("issuer" to "alice")).let {
assertEquals(2, countFieldsWithErrors(accessTokenValidation(it)))
assertEquals(2, countFieldsWithErrors(refreshTokenVerification(it)))
}
}

@Test
fun validateLists() {
data class Data(
Expand Down
Loading

0 comments on commit 6ec40a4

Please sign in to comment.