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
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)
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)))
Values
pub fn add_step_validator(
field: @internal Field(a, custom),
validator: fn(a) -> Result(a, List(FieldError(custom))),
) -> @internal 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: @internal Field(a, custom),
validators: List(fn(a) -> Result(a, List(FieldError(custom)))),
) -> @internal Field(a, custom)
Add several validators inside a single step. The whole batch only runs if no previous validator or parser errored. Within the batch, validators run in order and accumulate errors, like add_validators.
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: @internal Field(a, custom),
validator: fn(a) -> Result(a, List(FieldError(custom))),
) -> @internal 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: @internal Field(a, custom),
validators: List(fn(a) -> Result(a, List(FieldError(custom)))),
) -> @internal 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: @internal Field(a, custom),
mapper: fn(a) -> b,
) -> @internal 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) -> @internal Field(String, custom)
Starts a field pipeline by providing the field name.
Example
field.new("my_input")
pub fn optional(
base: @internal Field(a, custom),
) -> @internal 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.
Whitespace-only strings are validated normally.
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: @internal 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: @internal Field(String, custom),
parser: fn(String) -> Parsed(a, custom),
) -> @internal 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, custom) {
case int.parse(value) {
Ok(n) -> Parsed(n, [])
Error(_) -> Parsed(0, [MustBeInt])
}
})