Dataval

Data validation in Gleam.

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 Hex Docs

gleam add dataval@1

Examples

Validating a Single Field

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) -> Result(Int, List(field.FieldError(a))) {
  field.validate(age_field(), age)
}

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

Composing a Form out of Multiple Fields

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 respect a certain format (e.g. min/max length, alphanum characters)
  3. Uniqueness - check that the username doesn’t exist in database
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.

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

Development

gleam test  # Run the tests
Search Document