dataval/field

Core building blocks for composing a single field validation.

Example

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

Types

pub type Field(a, custom) {
  Field(name: String, run: fn(String) -> FieldState(a, custom))
}

Constructors

  • Field(name: String, run: fn(String) -> FieldState(a, custom))
pub type FieldError(custom) {
  MustNotBeEmpty
  MustBeInt
  MustBeFloat
  MustBeBool
  StringLengthTooShort(min: Int)
  StringLengthTooLong(max: Int)
  IntTooSmall(min: Int)
  IntTooLarge(max: Int)
  FloatTooSmall(min: Float)
  FloatTooLarge(max: Float)
  MustMatchRegexp
  CustomError(custom)
}

Constructors

  • MustNotBeEmpty
  • MustBeInt
  • MustBeFloat
  • MustBeBool
  • StringLengthTooShort(min: Int)
  • StringLengthTooLong(max: Int)
  • IntTooSmall(min: Int)
  • IntTooLarge(max: Int)
  • FloatTooSmall(min: Float)
  • FloatTooLarge(max: Float)
  • MustMatchRegexp
  • CustomError(custom)
pub type FieldState(a, custom) {
  FieldState(
    value: a,
    parser_failed: Bool,
    errors: List(FieldError(custom)),
  )
}

Constructors

  • FieldState(
      value: a,
      parser_failed: Bool,
      errors: List(FieldError(custom)),
    )
pub type Parsed(a, custom) {
  Parsed(value: a, errors: List(FieldError(custom)))
}

Constructors

  • Parsed(value: a, errors: List(FieldError(custom)))
pub type Parser(a, custom) =
  fn(String) -> Parsed(a, custom)
pub type Validator(a, custom) =
  fn(a) -> Result(a, List(FieldError(custom)))

Values

pub fn add_step_validator(
  field: Field(a, custom),
  validator: fn(a) -> Result(a, List(FieldError(custom))),
) -> Field(a, custom)

Add a validator that only runs if no previous validator or parser errored.

Use this to compose validation in multiple steps and for expensive checks like database lookups, external API calls, etc.

You can add multiple validators at once using add_step_validators.

Example

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")
}
pub fn add_step_validators(
  field: Field(a, custom),
  validators: List(fn(a) -> Result(a, List(FieldError(custom)))),
) -> Field(a, custom)

Add several validators inside a step. They will only run if no previous validator or parser errored.

Use this to compose validation in multiple steps and for expensive checks like database lookups, external API calls, etc.

Example

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_validators([
    validator.str_min(3),
    validator.str_max(20),
  ])
  // 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")
}
pub fn add_validator(
  field: Field(a, custom),
  validator: fn(a) -> Result(a, List(FieldError(custom))),
) -> Field(a, custom)
pub fn add_validators(
  field: Field(a, custom),
  validators: List(fn(a) -> Result(a, List(FieldError(custom)))),
) -> Field(a, custom)

Add multiple validators at once.

pub fn map(
  field: Field(a, custom),
  mapper: fn(a) -> b,
) -> Field(b, custom)
pub fn new(name: String) -> Field(String, custom)

A field starts as a raw string and is built as a pipeline:

pub fn optional(
  base: Field(a, custom),
) -> Field(option.Option(a), custom)

Make a field optional by wrapping its successful value in option.Some. An empty string becomes option.None and skips all parsers and validators.

This must be the last step of a pipeline because it changes the field’s type from Field(a, custom) to Field(Option(a), custom).

Example

new("name")
|> add_validator(validator.str_min(3))
|> add_validator(validator.str_max(10))
|> optional()
pub fn validate(
  field: Field(a, custom),
  value: String,
) -> Result(a, List(FieldError(custom)))
pub fn with_parser(
  field: Field(String, custom),
  parser: fn(String) -> Parsed(a, custom),
) -> Field(a, custom)

A parser can transform the data to another type.

Search Document