sift
Core validation functions — check fields, accumulate errors, compose validators.
Types
A validation error with path to the field and a human-readable message.
FieldError(path: ["email"], message: "required")
FieldError(path: ["address", "zip"], message: "must be 5 digits")
pub type FieldError {
FieldError(path: List(String), message: String)
}
Constructors
-
FieldError(path: List(String), message: String)
Accumulated validation result — value + errors collected so far
Using a tuple (not Result) enables the use pattern to run ALL validators
pub type Validated(a) =
#(a, List(FieldError))
Values
pub fn and(
v1: fn(a) -> Result(a, String),
v2: fn(a) -> Result(a, String),
) -> fn(a) -> Result(a, String)
Compose two validators — run both, accumulate errors from both.
let validator = s.min_length(1, "required") |> sift.and(s.email("invalid"))
pub fn check(
field: String,
value: a,
validator: fn(a) -> Result(a, String),
next: fn(a) -> #(b, List(FieldError)),
) -> #(b, List(FieldError))
Run a validator on a field value, accumulate errors, feeds into use.
use name <- sift.check("name", input.name, s.min_length(1, "required"))
use email <- sift.check("email", input.email, s.email("invalid"))
sift.ok(User(name:, email:))
pub fn check_all(
field: String,
value: a,
validators: List(fn(a) -> Result(a, String)),
next: fn(a) -> #(b, List(FieldError)),
) -> #(b, List(FieldError))
Run multiple validators on a field, accumulate all errors.
use name <- sift.check_all("name", input.name, [
s.non_empty("required"),
s.min_length(3, "too short"),
s.max_length(100, "too long"),
])
sift.ok(name)
pub fn check_optional(
field: String,
value: option.Option(a),
validator: fn(a) -> Result(a, String),
next: fn(option.Option(a)) -> #(b, List(FieldError)),
) -> #(b, List(FieldError))
Validate an Option value only when Some, skip when None.
use nickname <- sift.check_optional("nickname", input.nickname,
s.min_length(2, "too short"))
sift.ok(User(nickname:))
pub fn check_parse(
field: String,
value: a,
parser: fn(a) -> Result(b, c),
default: b,
msg: String,
next: fn(b) -> #(d, List(FieldError)),
) -> #(d, List(FieldError))
Parse a raw value and feed the result into the chain. On success, passes the parsed value to next. On failure, records a FieldError and passes the default to next so that subsequent fields still validate.
use age <- sift.check_parse("age", "42", int.parse, 0, "must be a number")
use age <- sift.check("age", age, i.min(13, "must be at least 13"))
sift.ok(age)
pub fn custom(
f: fn(a) -> Result(a, String),
) -> fn(a) -> Result(a, String)
Escape hatch for user-defined checks.
let even = sift.custom(fn(n: Int) {
case n % 2 == 0 {
True -> Ok(n)
False -> Error("must be even")
}
})
even(4) // -> Ok(4)
even(3) // -> Error("must be even")
pub fn each(
field: String,
items: List(a),
validator: fn(a) -> Result(a, String),
next: fn(List(a)) -> #(b, List(FieldError)),
) -> #(b, List(FieldError))
Validate every item in a list, accumulating indexed error paths.
Produces paths like ["tags", "0"], ["tags", "1"], etc.
use tags <- sift.each("tags", input.tags, s.non_empty("empty tag"))
// invalid items get paths like ["tags", "2"]
pub fn equals(
expected: a,
msg: String,
) -> fn(a) -> Result(a, String)
Value must equal the expected value.
let validator = sift.equals("yes", "must accept terms")
validator("yes") // -> Ok("yes")
validator("no") // -> Error("must accept terms")
pub fn nested(
field: String,
value: a,
validator_fn: fn(a) -> #(b, List(FieldError)),
next: fn(b) -> #(c, List(FieldError)),
) -> #(c, List(FieldError))
Run a sub-validator function, prefixing error paths with the field name.
use address <- sift.nested("address", input.address, validate_address)
// errors get paths like ["address", "zip"]
pub fn not(
validator: fn(a) -> Result(a, String),
msg: String,
) -> fn(a) -> Result(a, String)
Invert a validator — fail if it passes, pass if it fails.
let not_admin = sift.not(s.one_of(["admin"], ""), "cannot be admin")
pub fn ok(value: a) -> #(a, List(FieldError))
Wrap a final value into a Validated tuple with no errors
pub fn or(
v1: fn(a) -> Result(a, String),
v2: fn(a) -> Result(a, String),
) -> fn(a) -> Result(a, String)
Pass if either validator succeeds (try v1 first, then v2).
let validator = s.email("invalid") |> sift.or(s.url("invalid"))
pub fn validate(
validated: #(a, List(FieldError)),
) -> Result(a, List(FieldError))
Convert a Validated(a) to Result(a, List(FieldError)).
sift.ok(User(name: "Jo", email: "jo@example.com"))
|> sift.validate
// -> Ok(User(name: "Jo", email: "jo@example.com"))