<!--
SPDX-FileCopyrightText: 2026 Quentin BETTOUM <quentin@bettoum.fr>

SPDX-License-Identifier: EUPL-1.2
-->

# Dataval

Data validation in Gleam. Inspired by [formal](https://github.com/lpil/formal) and [toy](https://github.com/Hackder/toy).

Handle your data validation by composing forms while still being able to validate each field individually. This allows you to validate inputs as the user fills them while reusing the same validation logic for the whole form when it's submitted.

Improve the UX of your forms by defining validation steps.

[![Package Version](https://img.shields.io/hexpm/v/dataval)](https://hex.pm/packages/dataval)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://dataval.hexdocs.pm/)

```sh
gleam add dataval@1
```

## Examples

### Validate a Single Field

```gleam
import dataval/field
import dataval/parser
import dataval/validator

fn age_field() {
  field.new("age")
  |> field.with_parser(parser.int)
  |> field.add_validator(validator.int_min(18))
}

fn validate_age(age: String) {
  field.validate(age_field(), age)
}

pub fn main() {
  assert Ok(18) == validate_age("18")
  assert Error([field.IntTooSmall(18)]) == validate_age("17")
}
```

### Compose a Form out of Multiple Fields

```gleam
import dataval/field
import dataval/form
import dataval/parser
import dataval/validator
import gleam/dict

pub type CreateUser {
  CreateUser(age: Int, name: String)
}

fn age_field() {
  field.new("age")
  |> field.with_parser(parser.int)
  |> field.add_validator(validator.int_min(18))
}

fn name_field() {
  field.new("name")
  |> field.add_validator(validator.str_min(2))
}

fn create_user_form() {
  use age <- form.field(age_field())
  use name <- form.field(name_field())
  form.create(CreateUser(age:, name:))
}

pub fn main() {
  let user_form = create_user_form()

  assert Ok(CreateUser(20, "Toto"))
    == form.validate(user_form, [#("age", "20"), #("name", "Toto")])

  assert Error(
      dict.from_list([
        #("age", [field.IntTooSmall(18)]),
        #("name", [field.StringLengthTooShort(2)]),
      ]),
    )
    == form.validate(user_form, [#("age", "16"), #("name", "a")])
}
```

### Define Validation Steps

For example, for the username field of a registration form, we could define 3 steps:
1. Required - check that the field is not empty
2. Format - check that the field satisfies a certain format (e.g. min/max length, alphanum characters)
3. Uniqueness - check that the username doesn't exist in the database

Each step will only run when there was no error in the previous ones. It won't check the format if the user forgot to fill the field, and it wont check the uniqueness in the database if the format isn't satisfied.

```gleam
import dataval/field
import dataval/validator

pub type UsernameErrors {
  UsernameExists
}

fn username_field() {
  field.new("name")
  |> field.add_validator(validator.required)
  // If the field is not empty, check its length
  |> field.add_step_validator(validator.str_min(3))
  // If length is ok, check if the name is not taken
  |> field.add_step_validator(fn(username) {
    // Here you would typically make the database check
    case username == "already_used" {
      True -> Error([field.CustomError(UsernameExists)])
      False -> Ok(username)
    }
  })
}

pub fn main() {
  assert Ok("toto") == username_field() |> field.validate("toto")
  assert Error([field.MustNotBeEmpty]) == username_field() |> field.validate("")
  assert Error([field.StringLengthTooShort(3)])
    == username_field() |> field.validate("a")
  assert Error([field.CustomError(UsernameExists)])
    == username_field() |> field.validate("already_used")
}
```

[More examples can be found here.](https://codeberg.org/quentin-bettoum/dataval/src/branch/main/examples)

Further documentation can be found at <https://dataval.hexdocs.pm/>.

## Development

```sh
gleam test  # Run the tests
```
