From 3730d9fbc9756bbe5d9588c29c879f4c0381900a Mon Sep 17 00:00:00 2001 From: David Hoepelman Date: Mon, 18 Nov 2024 22:04:51 +0100 Subject: [PATCH 1/2] Add oneOf, anyOf validations --- .../io/konform/validation/ValidationResult.kt | 2 +- .../validation/types/LogicalCombinators.kt | 47 +++++++++++++++++++ .../konform/validation/types/ValidateAll.kt | 22 --------- 3 files changed, 48 insertions(+), 23 deletions(-) create mode 100644 src/commonMain/kotlin/io/konform/validation/types/LogicalCombinators.kt delete mode 100644 src/commonMain/kotlin/io/konform/validation/types/ValidateAll.kt diff --git a/src/commonMain/kotlin/io/konform/validation/ValidationResult.kt b/src/commonMain/kotlin/io/konform/validation/ValidationResult.kt index cba1163..5ee254f 100644 --- a/src/commonMain/kotlin/io/konform/validation/ValidationResult.kt +++ b/src/commonMain/kotlin/io/konform/validation/ValidationResult.kt @@ -86,7 +86,7 @@ public fun List>.flattenNonEmpty(): ValidationResult public fun List.flattenNotEmpty(): Invalid { require(isNotEmpty()) { "List is not allowed to be empty in flattenNonEmpty" } - return Invalid(map { it.errors }.flatten()) + return Invalid(this.flatMap { it.errors }) } public fun List>.flattenOrValid(value: T): ValidationResult = diff --git a/src/commonMain/kotlin/io/konform/validation/types/LogicalCombinators.kt b/src/commonMain/kotlin/io/konform/validation/types/LogicalCombinators.kt new file mode 100644 index 0000000..161806f --- /dev/null +++ b/src/commonMain/kotlin/io/konform/validation/types/LogicalCombinators.kt @@ -0,0 +1,47 @@ +package io.konform.validation.types + +import io.konform.validation.Invalid +import io.konform.validation.Valid +import io.konform.validation.Validation +import io.konform.validation.ValidationResult +import io.konform.validation.flattenOrValid + +private inline fun gatherInvalid( + validations: List>, + value: T, +): List { + val errors = mutableListOf() + for (validation in validations) { + val result = validation.validate(value) + if (result is Invalid) errors += result + } + return errors +} + +/** Validation that runs multiple validations in sequence. */ +public class ValidateAll( + private val validations: List>, +) : Validation { + override fun validate(value: T): ValidationResult = gatherInvalid(validations, value).flattenOrValid(value) + + override fun toString(): String = "ValidateAll(validation=$validations)" +} + +public class ValidationAny( + private val validations: List>, + private val createHint: (List) -> String = ::defaultCreateHint, +) : Validation { + override fun validate(value: T): ValidationResult { + val invalids = gatherInvalid(validations, value) + return if (invalids.size < validations.size) { + Valid(value) + } else { + Invalid() + } + } + + private companion object { + private fun defaultCreateHint(invalids: List): String = + "all validations failed: ${invalids.flatMap { invalid -> invalid.errors.map { it.message } }}" + } +} diff --git a/src/commonMain/kotlin/io/konform/validation/types/ValidateAll.kt b/src/commonMain/kotlin/io/konform/validation/types/ValidateAll.kt deleted file mode 100644 index 235c144..0000000 --- a/src/commonMain/kotlin/io/konform/validation/types/ValidateAll.kt +++ /dev/null @@ -1,22 +0,0 @@ -package io.konform.validation.types - -import io.konform.validation.Invalid -import io.konform.validation.Validation -import io.konform.validation.ValidationResult -import io.konform.validation.flattenOrValid - -/** Validation that runs multiple validations in sequence. */ -public class ValidateAll( - private val validations: List>, -) : Validation { - override fun validate(value: T): ValidationResult { - val errors = mutableListOf() - for (validation in validations) { - val result = validation.validate(value) - if (result is Invalid) errors += result - } - return errors.flattenOrValid(value) - } - - override fun toString(): String = "ValidateAll(validation=$validations)" -} From 3d5f3b63cb0b81869dd3374ae1c6f11b1091ec25 Mon Sep 17 00:00:00 2001 From: David Hoepelman Date: Wed, 25 Dec 2024 20:52:41 +0100 Subject: [PATCH 2/2] wip --- .../konform/validation/types/ValidateAll.kt | 1 + .../konform/validation/types/ValidationAny.kt | 37 +++++++++---------- .../validationbuilder/ValidateAnyTest.kt | 26 +++++++++++++ 3 files changed, 44 insertions(+), 20 deletions(-) create mode 100644 src/commonTest/kotlin/io/konform/validation/validationbuilder/ValidateAnyTest.kt diff --git a/src/commonMain/kotlin/io/konform/validation/types/ValidateAll.kt b/src/commonMain/kotlin/io/konform/validation/types/ValidateAll.kt index 95b77b9..44b8d6f 100644 --- a/src/commonMain/kotlin/io/konform/validation/types/ValidateAll.kt +++ b/src/commonMain/kotlin/io/konform/validation/types/ValidateAll.kt @@ -22,6 +22,7 @@ public class ValidateAll( override fun toString(): String = "ValidateAll(validation=$validations)" } +/** Validation that runs multiple validations in sequence and returns all validation errors. */ public class FailFastValidation( private val validations: List>, ) : Validation { diff --git a/src/commonMain/kotlin/io/konform/validation/types/ValidationAny.kt b/src/commonMain/kotlin/io/konform/validation/types/ValidationAny.kt index bf2d039..74a5789 100644 --- a/src/commonMain/kotlin/io/konform/validation/types/ValidationAny.kt +++ b/src/commonMain/kotlin/io/konform/validation/types/ValidationAny.kt @@ -4,34 +4,31 @@ import io.konform.validation.Invalid import io.konform.validation.Valid import io.konform.validation.Validation import io.konform.validation.ValidationResult - -private inline fun gatherInvalid( - validations: List>, - value: T, -): List { - val errors = mutableListOf() - for (validation in validations) { - val result = validation.validate(value) - if (result is Invalid) errors += result - } - return errors -} +import io.konform.validation.flattenNotEmpty +import io.konform.validation.flattenOrValid public class ValidationAny( private val validations: List>, - private val createHint: (List) -> String = ::defaultCreateHint, + private val aggregateInvalidResults: (List) -> Invalid = List::flattenNotEmpty, ) : Validation { override fun validate(value: T): ValidationResult { - val invalids = gatherInvalid(validations, value) - return if (invalids.size < validations.size) { - Valid(value) - } else { - Invalid() + val errors = mutableListOf() + for (validation in validations) { + when (val result = validation.validate(value)) { + // We only need 1 validation to succeed the "any" validation + is Valid -> return result + is Invalid -> errors + result + } } + return errors.flattenOrValid(value) } + override fun toString(): String = "ValidationAny(validation=$validations)" + private companion object { - private fun defaultCreateHint(invalids: List): String = - "all validations failed: ${invalids.flatMap { invalid -> invalid.errors.map { it.message } }}" + private fun defaultAggregateInvalidResults(invalids: List): Invalid { + val combinedErrors = + "all validations failed: ${invalids.flatMap { invalid -> invalid.errors.map { it.message } }}" + } } } diff --git a/src/commonTest/kotlin/io/konform/validation/validationbuilder/ValidateAnyTest.kt b/src/commonTest/kotlin/io/konform/validation/validationbuilder/ValidateAnyTest.kt new file mode 100644 index 0000000..3999030 --- /dev/null +++ b/src/commonTest/kotlin/io/konform/validation/validationbuilder/ValidateAnyTest.kt @@ -0,0 +1,26 @@ +package io.konform.validation.validationbuilder + +import io.konform.validation.Validation +import io.konform.validation.constraints.maxLength +import io.konform.validation.constraints.minLength +import kotlin.test.Test + +class ValidateAnyTest { + @Test + fun validateAny() { + val validation = + Validation { + oneOf( + "must be either length 5 or 10", + { + minLength(5) + maxLength(5) + }, + { + minLength(10) + maxLength(10) + }, + ) + } + } +}