Skip to content

Commit

Permalink
Add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
dhoepelman committed Nov 11, 2024
1 parent ae31c2c commit 508dde1
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 9 deletions.
16 changes: 12 additions & 4 deletions src/commonMain/kotlin/io/konform/validation/Validation.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
package io.konform.validation

import io.konform.validation.types.EmptyValidation
import io.konform.validation.types.ValidateAll

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

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

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

/** Combine a [List] of [Validation]s into a single one that returns all validation errors. */
public fun <T> List<Validation<T>>.flatten(): Validation<T> =
when (size) {
0 -> EmptyValidation
1 -> first()
else -> ValidateAll(this)
}
13 changes: 13 additions & 0 deletions src/commonMain/kotlin/io/konform/validation/ValidationError.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.konform.validation

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

public companion object {
internal operator fun invoke(
dataPath: String,
message: String,
): ValidationError = PropertyValidationError(dataPath, message)
}
}
38 changes: 33 additions & 5 deletions src/commonMain/kotlin/io/konform/validation/ValidationResult.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
package io.konform.validation

import io.konform.validation.kotlin.Path

public interface ValidationError {
public val dataPath: String
public val message: String
}
import kotlin.jvm.JvmName

internal data class PropertyValidationError(
override val dataPath: String,
Expand Down Expand Up @@ -68,3 +64,35 @@ public data class Valid<T>(
override val errors: List<ValidationError>
get() = emptyList()
}

internal fun <T> List<ValidationResult<T>>.flattenNonEmpty(): ValidationResult<T> {
require(isNotEmpty()) { "List<ValidationResult> is not allowed to be empty in flattenNonEmpty" }
val invalids = filterIsInstance<Invalid>()
return if (invalids.isEmpty()) {
first() as Valid
} else {
invalids.flattenNotEmpty()
}
}

internal fun List<Invalid>.flattenNotEmpty(): Invalid {
require(isNotEmpty()) { "List<Invalid> is not allowed to be empty in flattenNonEmpty" }
val merged = mutableMapOf<String, List<String>>()
for (invalid in this) {
val added =
invalid.internalErrors.mapValues {
merged.getOrElse(it.key, ::emptyList) + it.value
}
merged += added
}
return Invalid(merged)
}

internal fun <T> List<ValidationResult<T>>.flattenOrValid(value: T): ValidationResult<T> =
takeIf { isNotEmpty() }
?.flattenNonEmpty()
?.takeIf { it is Invalid }
?: Valid(value)

@JvmName("flattenOrValidInvalidList")
internal fun <T> List<Invalid>.flattenOrValid(value: T): ValidationResult<T> = if (isNotEmpty()) flattenNonEmpty() else Valid(value)
13 changes: 13 additions & 0 deletions src/commonMain/kotlin/io/konform/validation/path/ValidationPath.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.konform.validation.path

import io.konform.validation.kotlin.Path

/** Represents a path to a validation. */
public data class ValidationPath(
// val segments: List<PathSegment>,
val dataPaths: List<String>,
) {
public companion object {
public fun fromAny(vararg validationPath: Any): ValidationPath = ValidationPath(validationPath.map { Path.toPath(*validationPath) })
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.konform.validation.types

import io.konform.validation.Valid
import io.konform.validation.Validation
import io.konform.validation.ValidationResult

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

override fun toString(): String = "EmptyValidation"

override fun equals(other: Any?): Boolean = other === this

override fun hashCode(): Int = 912378
}
22 changes: 22 additions & 0 deletions src/commonMain/kotlin/io/konform/validation/types/ValidateAll.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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<T>(
private val validations: List<Validation<T>>,
) : Validation<T> {
override fun validate(value: T): ValidationResult<T> {
val errors = mutableListOf<Invalid>()
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)"
}
55 changes: 55 additions & 0 deletions src/commonTest/kotlin/io/konform/validation/ListValidationTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package io.konform.validation

import io.konform.validation.jsonschema.minimum
import io.konform.validation.types.EmptyValidation
import io.konform.validation.types.ValidateAll
import io.kotest.assertions.konform.shouldBeInvalid
import io.kotest.assertions.konform.shouldBeValid
import io.kotest.assertions.konform.shouldContainExactlyErrors
import io.kotest.assertions.konform.shouldContainOnlyError
import io.kotest.matchers.types.shouldBeInstanceOf
import io.kotest.matchers.types.shouldBeSameInstanceAs
import kotlin.test.Test

class ListValidationTest {
private val validation1 =
Validation<Int> {
minimum(0)
}
private val validation2 =
Validation<Int> {
minimum(10)
}

@Test
fun flattenNone() {
val result = listOf<Validation<Int>>().flatten()

result shouldBeSameInstanceAs EmptyValidation
result shouldBeValid 0
result shouldBeValid -100
}

@Test
fun flattenOne() {
val result = listOf(validation1).flatten()

result shouldBeSameInstanceAs validation1
result shouldBeValid 0
result shouldBeInvalid -100
}

@Test
fun flattenMore() {
val result = listOf(validation1, validation2).flatten()

result.shouldBeInstanceOf<ValidateAll<Int>>()

result shouldBeValid 10
(result shouldBeInvalid 5) shouldContainOnlyError ValidationError("", "must be at least '10'")
(result shouldBeInvalid -1).shouldContainExactlyErrors(
ValidationError("", "must be at least '0'"),
ValidationError("", "must be at least '10'"),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import io.kotest.matchers.collections.shouldNotContain
import io.kotest.matchers.nulls.shouldBeNull
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe

infix fun <T> Validation<T>.shouldBeValid(value: T) = this should beValid(value)

Expand Down Expand Up @@ -102,3 +103,7 @@ fun Invalid.shouldContainExactlyErrors(vararg errors: Pair<String, String>) =
this.errors shouldContainExactlyInAnyOrder errors.map { PropertyValidationError(it.first, it.second) }

infix fun Invalid.shouldContainExactlyErrors(errors: List<ValidationError>) = this.errors shouldContainExactlyInAnyOrder errors

infix fun Invalid.shouldContainOnlyError(error: ValidationError) {
this.errors shouldBe listOf(error)
}

0 comments on commit 508dde1

Please sign in to comment.