Akkurate 0.10.0 Help

Getting started

This article will show you how to install Akkurate and write your first validation code.

Installation

Akkurate is shipped with two dependencies; one is the library, the other one is a compiler plugin based on KSP. Follow the installation instructions below, according to your project structure.

Install in a single-platform project

  1. Add KSP to your plugin list; make sure to use the appropriate version, depending on the Kotlin version you're using.

    plugins { kotlin("jvm") version "2.0.20" id("com.google.devtools.ksp") version "2.0.20-1.0.25" }
  2. Add the dependencies and register the compiler plugin through KSP.

    dependencies { implementation("dev.nesk.akkurate:akkurate-core:0.10.0") ksp("dev.nesk.akkurate:akkurate-ksp-plugin:0.10.0") }

Install in a multiplatform project

  1. Add KSP to your plugin list; make sure to use the appropriate version, depending on the Kotlin version you're using.

    plugins { kotlin("multiplatform") version "2.0.20" id("com.google.devtools.ksp") version "2.0.20-1.0.25" }
  2. Register the compiler plugin for the common target:

    dependencies { add("kspCommonMainMetadata", "dev.nesk.akkurate:akkurate-ksp-plugin:0.10.0") } tasks { withType<KotlinCompilationTask<*>>().configureEach { if (name != "kspCommonMainKotlinMetadata") { dependsOn("kspCommonMainKotlinMetadata") } } }
  3. Configure the commonMain source set:

    kotlin.sourceSets.commonMain { dependencies { // Add Akkurate dependencies implementation("dev.nesk.akkurate:akkurate-core:0.10.0") } // Include the code generated by the KSP plugin kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin") }

Basic usage

Akkurate requires two elements to validate an object:

  • the @Validate annotation, to mark each class you need to validate;

  • and the Validator interface, to define validation rules for your classes.

Here we want to validate a Book class, with its title and release date. Let's create it and add the @Validate annotation to it:

@Validate data class Book(val title: String, val releaseDate: LocalDateTime)

This annotation marks the class for the compiler processor, so it can generate validatable accessors for it. Now, build the project with ./gradlew build (or Build | Build Project in IntelliJ IDEA) to trigger code generation. We will explain this whole concept a bit further.

To apply constraints, you need to instantiate a Validator with a lambda, let's start with an empty one:

val validateBook = Validator<Book> { // this: Validatable<Book> }

The lambda receiver is a Validatable<Book>, a generic class wrapping each value you access during validation. Its job is to track the path of each value, provide a DSL to ease validation, and act as a register for the constraints you apply to the value.

Now, let's constrain the title and the release date; the former must contain characters, and the latter can't be more than one year later after the current date:

val validateBook = Validator<Book> { title.isNotEmpty() val oneYearLater = LocalDateTime.now().plusYears(1) releaseDate.isBeforeOrEqualTo(oneYearLater) }

You might have noticed that title and releaseDate aren't properties of the Validatable<T> class, but it works anyway. This is why we had to annotate our data class with @Validate and trigger the generation of validatable accessors; those act as a bridge between the Validatable<T> class and the underlying value.

It is time to validate a book with our new validator:

val invalidBook = Book( title = "", releaseDate = LocalDateTime.now().plusYears(3) ) when (val result = validateBook(invalidBook)) { is ValidationResult.Success -> { println("The book is valid: ${result.value}") } is ValidationResult.Failure -> { println("The book is invalid, here are the errors:") for ((message, path) in result.violations) { println(" - $path: $message") } } }

Once validation is done, it returns a ValidationResult, which is a sealed interface composed of two classes:

  • ValidationResult.Success: the validation has succeeded, it contains the validated value.

  • ValidationResult.Failure: the validation has failed, it contains a violation list. Each violation contains the path of the property, and the message describing why it failed.

Here, the validation failed, so we got the following lines in the output stream:

The book is invalid, here are the errors: - [title]: Must not be empty - [releaseDate]: Must be before or equal to "2024-08-11T18:56:04.0"

Here is the whole code of this example, feel free to play with it in your editor:

package dev.nesk.akkurate.demo import dev.nesk.akkurate.ValidationResult import dev.nesk.akkurate.Validator import dev.nesk.akkurate.annotations.Validate import dev.nesk.akkurate.constraints.builders.* import dev.nesk.akkurate.demo.validation.accessors.* import java.time.LocalDateTime @Validate data class Book(val title: String, val releaseDate: LocalDateTime) val validateBook = Validator<Book> { title.isNotEmpty() val oneYearLater = LocalDateTime.now().plusYears(1) releaseDate.isBeforeOrEqualTo(oneYearLater) } fun main() { val invalidBook = Book( title = "", releaseDate = LocalDateTime.now().plusYears(3) ) when (val result = validateBook(invalidBook)) { is ValidationResult.Success -> { println("The book is valid: ${result.value}") } is ValidationResult.Failure -> { println("The book is invalid, here are the errors:") for ((message, path) in result.violations) { println(" - $path: $message") } } } }
Last modified: 24 September 2024