Coval is a library that provides a simple and composable way to validate data in Kotlin.
It uses functions as an alias for validation rules, and allow you to combine them, creating complex validation rules. It also provides tooling for how to handle any error produced by a validation, and makes combined validations easy to read and understand.
Coval relies on Arrow.Either for its flow control.
The philosophy behind Coval is to make validation as simple as possible, while still being powerful and flexible.
Coval itself is built around the idea of being able to define rules as named variables, and pass them on, and transform then into new rules through combinators. A ValidatorFunction, is just a type alias for this type:
typealias ValidatorFunction<E, T> = (T) -> Either<List<E>, T>
val rule = {
input -> if (something) input.right()
else listOf("Error").left()
}This is a typical rule, and produces what is a ValidatorFunction, it is the base of the library, and can be used to create Validators.
rule.toValidator()Validation rules are supposed to be structured, named rules, that allow you to write validation at a high level, close to natural langauge.
Assume we have two rules, and a simple Person type defined. let's call them validateName and validateAge. Coval then lets you combine these rules if that makes sense.
val validateNameAndAge = validateName and validateAgeis perfectly valid, and lets you see what is supposed to happen just through naming.
Caution
Coval is designed to be used for pure functions, and adding effectful code to your validation rules will make them harder to test and reason about. It is recommended to keep your validation rules as pure as possible, and handle side effects in a separate layer.
Validators contains functions for handling validation, and allow you to shape the flow of your validation.
For instance through aggregating errors.
.toValidator() accepts a aggregator-function of type List<E> -> List<E>, this can be used to join errors.
This can be useful both for making error messages, or for joining replacement values in whichever way makes the most sense.
Example:
data class Person(val name: String, val age: Int)
val person = Person("John", 30)
val validateName = { p: Person -> if (p.name.isNotEmpty()) p.right() else listOf("Name is empty").left() }.toValidator()
val validateAge = { p: Person -> if (p.age > 0) p.right() else listOf("Age is invalid").left() }.toValidator()
val validatePerson = validateName then validateAge // If person has no name, age is not validated
val validateAgeOrName = validateName or validateAge // If person has no name, age is validatedSince any resulting validator from a combination will just be a new validator, this can go on forever.
A Validator can also force a transformation on its output value, to handle anticipated errors or to transform the output value.
val john = Person("John", 30)
val validateName = { p: Person ->
when(p.name) {
"John" -> p.copy(name = "Jon").right()
"Jon" -> listOf("Name can't be Jon").left()
else -> listOf("Name is invalid").left()
}
}.toValidator()
// Here the result of the first validation is looked at by the second validation, which fails.
val validatePerson = validateName then validateName
// Both validations will run on the original input, and neither will fail.
val validatePerson = validateName and validateAge Each Combinator will return a new Validator, retaining the original validators for reuse.
A combinator has built in structure for joining errors together.
This is a function of type (List<E>) -> List<E> which can be used to join errors together in a more readable way.
for certain combinators you may want to join error messages, or even replacement values in a custom way.
Meaning if you have a Validator of type Validator<A, A>, you can set up failed validations to return replacement values.
// Will return "Name is empty and Age is invalid" when ran.
// - the joining function is enclosed within the validators, and will be called when the validator is run.
val validatePerson = (validateName or validateAge)
.joinErrors { errors: List<String> -> listOf(errors.joinToString(separator = " and ")) }