validated

This module provides the Validated type and associated functions to accumulate errors in an ergonomic way.

Example

fn validate_form(email: String, age: Int) -> Validated(Form, String) {
  use email, is_valid <- v.do(validate_email(email))
  use email , _ <- v.do_if(is_valid, check_database(email), email)
  use age, _ <- v.do(validate_age(age))
  Valid(Form(email:, age:))
}

validated_form("lucy@example.com", 20)
// -> Valid(Form("lucy@example.com", 20))

validated_form("exists@example.com", 5)
// -> Invalid(Form("", 0), ["email address already exists", "must be 18 or older"])

validated_form("asdf", 5)
// -> Invalid(Form("", 0), ["not a valid email", "must be 18 or older"])

This API is possible because a Validated requires a “default” value in case of failure. That way, the default value is passed through to continue the validation.

Since a value within a validation block may or may not be the default value, it is important to never perform side-effects inside the validation.

Types

Validated represents the result of something that may succeed or not. It is similar to Result, except that it will accumulate multiple errors instead of stopping at the first error. Valid means it was successful, Invalid means it was not successful.

pub type Validated(a, e) {
  Valid(a)
  Invalid(a, List(e))
}

Constructors

  • Valid(a)
  • Invalid(a, List(e))

    Invalid requires a default value to enable validation in use expressions

A function that takes some input and returns a Validated

pub type Validator(in, out, error) =
  fn(in) -> Validated(out, error)

Functions

pub fn all(
  validateds: List(Validated(a, b)),
) -> Validated(List(a), b)

Combines a list of Validateds into a single Validated. If all elements in the list are Valid then the function returns a Valid holding the list of values. Otherwise an Invalid is returned that combines all the errors. Valid([]) is returned if the list is empty.

pub fn bit_array(
  value: Result(BitArray, a),
) -> Validated(BitArray, a)

Creates a Validated from a Result. It uses an empty BitArray as the default value in case of failure.

pub fn bool(value: Result(Bool, a)) -> Validated(Bool, a)

Creates a Validated from a Result. It uses False as the default value in case of failure.

pub fn dict(
  value: Result(Dict(a, b), c),
) -> Validated(Dict(a, b), c)

Creates a Validated from a Result. It uses an empty Dict as the default value in case of failure.

pub fn do(
  validated: Validated(a, b),
  next: fn(a, Bool) -> Validated(c, b),
) -> Validated(c, b)

“Updates” a Valid value by passing its value, and a Bool indicating whether or not it is valid, to a function that yields a Validated, and returning the yielded Validated. (This may “replace” the Valid with an Invalid.)

If the input is an Invalid rather than an Valid, the function is still called using the default value from the Invalid. If the function succeeds, the Invalid’s default is replaced with the Valid value. If the function fails, the first Invalid errors are combined with the returned Invalid errors.

The Bool return value can be used to decide to short circuit further validation. See the do_if function for this use case. If you want to run all the validations regardless, you can ignore this value.

do can be used in use expressions to ergonomically validate fields of a record

Examples

use a, _ <- do(Valid(1))
use b, _ <- do(Valid(2))
Valid(#(a, b))
// -> Valid(#(1, 2))
use a, _ <- do(Invalid(0, [Nil]))
use b, _ <- do(Valid(2))
use c, _ <- do(Invalid(0, [Nil]))
Valid(#(a, b, c))
// -> Invalid(#(0, 2, 0), [Nil, Nil])
use a, is_valid <- do(Invalid(0, [Nil]))
io.debug(is_valid)
// -> False
use b, is_valid <- do(Valid(2))
io.debug(is_valid)
// -> True
use c, is_valid <- do(Invalid(0, [Nil]))
io.debug(is_valid)
// -> False
Valid(#(a, b, c))
// -> Invalid(#(0, 2, 0), [Nil, Nil])
pub fn do_if(
  when requirements: Bool,
  do validated: Validated(a, b),
  otherwise default: a,
  next next: fn(a, Bool) -> Validated(c, b),
) -> Validated(c, b)

Like do, but only checks the input Validated if the requirements are True, otherwise it will return the provided default value as an Invalid with no errors.

This function works well in use expressions with do.

Examples

use a, is_valid <- do(Invalid(0, [Nil]))
use a, _ <- do_if(is_valid, Valid(1), a)
use b, _ <- do(Valid(2))
Valid(#(a, b))
// -> Invalid(#(0, 2), [Nil])
use username, is_valid <- do(meets_requirements(username))
use username, _ <- do_if(is_valid, check_availability(username), username)
use age, _ <- do(Valid(30))
Valid(User(username, age))
pub fn flatten(
  validated: Validated(Validated(a, b), b),
) -> Validated(a, b)

Merges a nested Validated into a single layer.

pub fn float(value: Result(Float, a)) -> Validated(Float, a)

Creates a Validated from a Result. It uses 0.0 as the default value in case of failure.

pub fn int(value: Result(Int, a)) -> Validated(Int, a)

Creates a Validated from a Result. It uses 0 as the default value in case of failure.

pub fn is_invalid(validated: Validated(a, b)) -> Bool

Checks whether the Validated is Invalid

pub fn is_valid(validated: Validated(a, b)) -> Bool

Checks whether the Validated is Valid

pub fn list(value: Result(List(a), b)) -> Validated(List(a), b)

Creates a Validated from a Result. It uses an empty list as the default value in case of failure.

pub fn map(
  validated: Validated(a, b),
  f: fn(a) -> c,
) -> Validated(c, b)

Updates a value held within the Valid of a Validated by calling a given function on it. If the Validated is an Invalid rather than Valid, the function is called on the default value.

pub fn map_error(
  validated: Validated(a, b),
  f: fn(List(b)) -> List(c),
) -> Validated(a, c)

Updates a value held within the Invalid of a Validated by calling a given function on it. If the result is Valid rather than Invalid the function is not called and the result stays the same.

pub fn optional(
  value: Result(Option(a), b),
) -> Validated(Option(a), b)

Creates a Validated from a Result. It uses None as the default value in case of failure.

pub fn replace(
  validated: Validated(a, b),
  value: c,
) -> Validated(c, b)

Replace the value within a Validated

pub fn result(value: Result(a, b), default: a) -> Validated(a, b)

Creates a Validated from a Result. It requires a default value in case of failure.

pub fn run(
  validator: fn(a) -> Validated(b, c),
  input: a,
) -> Validated(b, c)

Run a Validator function.

pub fn run_all(
  validators: List(fn(a) -> Validated(b, c)),
  input: a,
) -> Validated(Nil, c)

Run all the Validators in order on the given input. It will accumulate all the errors from all of the Validators.

If there are no errors, or if the list is empty, Valid(Nil) is returned.

pub fn string(value: Result(String, a)) -> Validated(String, a)

Creates a Validated from a Result. It uses an empty string as the default value in case of failure.

pub fn to_result(
  validated: Validated(a, b),
) -> Result(a, List(b))

Convert a Validated into a Result

pub fn try_map(
  validated: Validated(a, b),
  default: c,
  f: fn(a) -> Result(c, b),
) -> Validated(c, b)

“Updates” a Valid value by passing its value to a function that yields a Result, converting that Result to a Validated, and returning the yielded Validated. (This may “replace” the Valid with an Invalid.)

If the input is an Invalid rather than an Valid, the function is still called using the default value from the Invalid. If the function succeeds, the Invalid’s default is replaced with the Ok value. If the function fails, the first Invalid errors are combined with the returned Error, and the default value is replaced with the provided default.

pub fn unwrap(validated: Validated(a, b)) -> a

Return the Valid value of the validated, or the default value if it is Invalid.

Search Document