dataval/field

Core building blocks for composing the validation of a single field.

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

A form is a function from raw input values to a FormState. Build forms with form.field and form.create.

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

Constructors

  • Field(name: String, run: fn(String) -> FieldState(a, custom))

Errors that can be produced by built-in validators and parsers. The custom parameter is used for application-specific errors via the CustomError variant.

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)

Internal state of a field during validation. parser_failed is set to True when a parser produced errors, preventing subsequent validators from running on a dummy value.

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)),
    )

A parsed value. custom is the type carried by CustomError variants; it is unused by parsers that only produce built-in errors.

pub type Parsed(a, custom) {
  Parsed(value: a, errors: List(FieldError(custom)))
}

Constructors

  • Parsed(value: a, errors: List(FieldError(custom)))

Result of a parser. Parsers always return a value of the target type, even when the input is invalid, so that the pipeline remains composable.

pub type Parser(a, custom) =
  fn(String) -> Parsed(a, custom)

A function that validates a parsed value.

Returns Ok(value) if validation passes, or Error(errors) if it fails. Multiple validators can be composed on a field using add_validator, add_validators, add_step_validator, and add_step_validators.

Example

fn is_even(value: Int) -> field.Validator(Int, MyError) {
  fn(n) {
    case n % 2 == 0 {
      True -> Ok(n)
      False -> Error([field.CustomError(NotEven)])
    }
  }
}
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)

Add a validator.

Example

// We want a description length of minimum 10 characters, and maximum 100
field.new("description")
|> field.add_validator(validator.str_min(10))
|> field.add_validator(validator.str_max(100))
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.

Example

// We want a description length of minimum 10 characters, and maximum 100
field.new("description")
|> field.add_validators([
  validator.str_min(10),
  validator.str_max(100)
])
pub fn map(
  field: Field(a, custom),
  mapper: fn(a) -> b,
) -> Field(b, custom)

Transform the value in the validation pipeline

Example

assert Ok("Hello Gleam")
  == field.new("name")
  |> field.map(fn(name) { "Hello " <> name })
  |> field.validate("Gleam")

assert Ok(4)
  == field.new("number")
  |> field.with_parser(parser.int)
  |> field.map(int.multiply(_, 2))
  |> field.validate("2")

assert Ok(42)
  == field.new("number")
  |> field.with_parser(parser.float)
  |> field.map(fn(n) { n *. 10.0 })
  |> field.map(float.round)
  |> field.validate("4.2")
pub fn new(name: String) -> Field(String, custom)

Starts a field pipeline by providing the field name.

Example

field.new("my_input")
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

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

Validate value on a field by passing a value to the validation pipeline.

Example

assert Ok(4)
  == field.new("number")
  |> field.with_parser(parser.int)
  |> field.map(int.multiply(_, 2))
  |> field.validate("2")
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.

It always returns a value of the target type, even when the input is invalid. It’s a deliberate trade-off to keep the pipeline composable and type-safe.

Example

field.new("my_input")
|> field.with_parser(fn(value: String) -> Parsed(Int, errors) {
  case int.parse(value) {
    Ok(n) -> Parsed(n, [])
    Error(_) -> Parsed(0, [MustBeInt])
  }
})
Search Document