Skip to content

OleBerg1/Coval

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Apache 2.0 License

Coval

Composable validation for Kotlin

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 validateAge

is 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 validated

Since 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 ")) }

About

Composable validation for Kotlin

Resources

License

Stars

1 star

Watchers

1 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages