Skip to content

resoluteworks/validk

Repository files navigation

Validk

GitHub release (latest by date) Coveralls

Validk is a validation framework for Kotlin JVM designed with these goals in mind:

  • Typesafe DSL to defining validation rules
  • No annotations or "magic"
  • Value-aware and conditional validation rules (aka dynamic validation)
  • Zero dependencies

Documentation:

Dependency

implementation "io.resoluteworks:validk:${validkVersion}"

Quick start

data class Employee(val name: String, val email: String?)
data class Organisation(val name: String, val employees: List<Employee>)

val organisationValidation = Validation {
    // Organisation name should be at least 5 characters long
    Organisation::name { minLength(5) }

    Organisation::employees each {
        // Each employee should have a name that is at least 10 characters long.
        Employee::name { minLength(10) }

        // An employee can have an email address, but it's not required.
        // When present, it should be a valid email.
        Employee::email ifNotNull { email() }
    }
}

val org = Organisation(
    name = "ACME",
    employees = listOf(Employee("John", "[email protected]"), Employee("Hannah Johnson", "hanna"))
)
val result: ValidationResult<Organisation> = organisationValidation.validate(org)
when (result) {
    is ValidationResult.Success -> println("Validation success")
    is ValidationResult.Failure -> result.allErrors.forEach { println(it) }
}

The call organisationValidation.validate(org) returns a ValidationResult<Organisation> which can be either a ValidationResult.Success or a ValidationResult.Failure. The ValidationResult.Failure returns the details of the validation errors.

The code above would print the following:

ValidationError(path=name, message=Must be at least 5 characters long)
ValidationError(path=employees[0].name, message=Must be at least 10 characters long)
ValidationError(path=employees[1].email, message=Must be a valid email)

Error messages

Error messages can be customised with any of the following constructs.

// Dynamic error message
Employee::email {
    email() message { value -> "Invalid email address: $value" }
}

// Static error message
Employee::email {
    email() message "This emails address is invalid"
}

Custom constraints

You can define custom constraints by calling addConstraint inside a validation block.

Employee::name{
    addConstraint("Must start with uppercase letter") {
        it.first().isUpperCase() == true
    }
}

Dynamic validation

There are several options for defining validation rules which apply in a specific context or when the value being validated meets a certain condition.

withValue

The withValue lambda receives the object being validated and allows you to define constraints based on its properties or state.

data class Entity(
    val type: String,
    val registeredOffice: String,
    val proofOfId: String
)

Validation<Entity> {
    withValue { entity ->
        when (entity.type) {
            "PERSON" -> Entity::proofOfId { minLength(10) }
            "COMPANY" -> Entity::registeredOffice { minLength(5) }
        }
    }
}

whenIs

The whenIs construct allows you to define constraints based on specific values for a property.

Validation {
    Entity::entityType.whenIs("PERSON") {
        Entity::proofOfId { minLength(10) }
    }

    Entity::entityType.whenIs("COMPANY") {
        Entity::registeredOffice { minLength(5) }
    }
}

Convert validation result

A ValidationResult can be converted to a custom type using the map function. This is typically useful when a custom application state is required as the result of a validation. A common example would be a web application that would return a different HTTP response or status code based on the validation result.

val httpResponse = validation.validate(personForm).map {
    success { person ->
        Response(HttpStatus.OK, person)
    }
    error { person, errors ->
        Response(HttpStatus.BAD_REQUEST, person, errors)
    }
}

Fail-fast validation

It's often required to only return the first failure message (first failed constraint) when validating a property. This is the case, for example, when displaying user errors in a UI, and when the order of the constraints implies the next ones would fail anyway (and thus don't need checking).

For example, let's say that we have an email field that's both required and needs to be a valid email. In this case, if a notBlank() fails, that means that email() will fail as well. In this case, we'd like to return only the first error message, which would be

"Email is required"

rather than

["Email is required", "This is not a valid email"].

We call this fail-fast validation and it's enabled by default. Fail-fast validation can be configured when creating the Validation object. The example below will check all the constraints and return all the errors.

Validation {
    failFast(false)
    Person::name {
        notBlank()
        matches("[a-zA-Z]+ [a-zA-Z]+")
    }
}

When turning fail-fast off, you can still opt to only select the first error message post-validation, by using ValidationErrors.error(propertyPath). See ValiationErrors docs for more details.

ValidObject

ValidObject provides a basic mechanism for storing the validation logic within the object itself.

data class Person(val name: String, val email: String) : ValidObject<Person> {
    override val validation: Validation<Person> = Validation {
        Person::name { minLength(10) }
        Person::email { email() }
    }
}

val validationResult = Person("John Smith", "[email protected]").validate()

License

Apache 2.0 License

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Sponsor this project

 

Packages

No packages published

Contributors 2

  •  
  •