Depot.Gate
Depot.Gate: typed validation pipeline for web forms and data input.
A Gate accumulates validation results over raw input params without short-circuiting — all validators run, all errors are collected. At the end you have either a clean set of cast changes ready to persist, or a structured error map ready to render in a form.
The name "Gate" was chosen because data must pass through the gate before entering the system. A valid Gate is the only thing Depot.insert accepts — unvalidated data cannot pass.
Basic usage:
import Depot.Gate
fn user_gate(params) do
cast(params, ["name", "email", "age"])
|> validate_required(["name", "email"])
|> validate_length("name", [LenMin(2), LenMax(100)])
|> validate_format("email", "@")
|> validate_number("age", [NumMin(13), NumMax(150)])
end
let gate = user_gate(conn_params)
if is_valid(gate) then
-- use get_change(gate, "name") etc. for persistence
Ok(gate)
else
Err(errors(gate))
endDB constraint hints (resolved after Depot.insert/update):
cast(params, ["email"])
|> validate_required(["email"])
|> unique_constraint("email", [ConstraintName("users_email_index"),
ConstraintMessage("has already been taken")])Types
Functions
Adds an error for a field and marks the form invalid.
form |> Depot.Gate.add_error("email", "has invalid format")Merges base + changes into a single assoc list. For Gate, this is the effective record after all changes are applied.
Depot.Gate.apply_changes(gate) -- [("name", "Alice"), ("email", "[email protected]"), ...]Maps a database constraint name back to a field error.
Called by Depot.insert/update when a DB error occurs.
Depot.Gate.apply_constraint_error(form, "users_email_index")Returns the base entries of a Gate (the original record as an assoc list). For a v1 Gate, returns the original params.
Depot.Gate.base(gate)Create a Gate from raw params, keeping only the allow-listed fields.
Values remain as strings; type coercion (e.g. "42" -> Int) is the caller's responsibility via put_change.
let form = Depot.Gate.cast(params, ["name", "email", "age"])Create a record-based Gate from a base record, incoming params record, and an allow-list of field names.
Only fields in the allow-list are considered, and only values that differ from base are recorded as changes. This enables efficient UPDATE queries that only SET changed columns.
let blank = Depot.Schema.blank(schema())
let gate = Depot.Gate.cast_record(blank, params, ["name", "email", "role"])Insert flow: base is Depot.Schema.blank(schema), so every param that differs from the default is a change.
Update flow: base is a DB record, so only actual modifications appear in changes.
Returns true if the given field has a change recorded.
Depot.Gate.changed(gate, "name") -- true or falseReturns the list of field names that have changes.
Depot.Gate.changed_fields(gate) -- ["name", "email"]Registers a check constraint hint.
form |> Depot.Gate.check_constraint("age",
[ConstraintName("users_age_check"),
ConstraintMessage("must be 13 or older")])Removes a change from the form (e.g. after hashing a password).
form |> Depot.Gate.delete_change("password")Returns all errors as a list of (field, message) pairs.
Depot.Gate.errors(form) -- [("email", "has invalid format"), ...]Registers a foreign key constraint hint.
form |> Depot.Gate.foreign_key_constraint("user_id",
[ConstraintName("posts_user_id_fkey")])Returns the change (cast value) for a field, or None if not present.
Depot.Gate.get_change(form, "email")Returns all error messages for a specific field.
Depot.Gate.get_errors(form, "email") -- ["has invalid format", ...]Returns the current value of a field, checking changes first then base/params.
For v1 Gates, falls back to params. For v2 Gates (cast_record), falls back to the base record entries.
Depot.Gate.get_field(form, "name") -- Some("Alice") or NoneReturns true if the form has no validation errors.
if Depot.Gate.is_valid(form) do persist(form) else render_errors(form) endRegisters a no-association constraint hint.
form |> Depot.Gate.no_assoc_constraint("posts",
[ConstraintName("posts_user_id_fkey")])Sets a change value on the form.
form |> Depot.Gate.put_change("role", "admin")Registers a unique constraint hint.
When Depot.insert/update encounters a unique violation on the named constraint, it maps the error back to this field with this message.
form
|> Depot.Gate.unique_constraint("email",
[ConstraintName("users_email_index"),
ConstraintMessage("has already been taken")])Validates that a field has been accepted (checkbox style: value "true" or "1").
form |> Depot.Gate.validate_acceptance("terms_of_service")Applies a custom validator function to a field.
The validator receives the field value and returns: None -- valid (no error) Some(msg) -- invalid, add this error message
form |> Depot.Gate.validate_change("username", fn v ->
if String.byte_size(v) < 3 do Some("too short") else None end
)Validates that a field matches its confirmation counterpart.
For field "password", checks that "password_confirmation" has the same value.
form |> Depot.Gate.validate_confirmation("password")Validates that a field value is NOT in the excluded set.
form |> Depot.Gate.validate_exclusion("username", ["root", "admin"])Validates that a field value matches a regex pattern.
form |> Depot.Gate.validate_format("email", "@")
form |> Depot.Gate.validate_format("slug", "^[a-z0-9-]+$")Absent fields are silently skipped.
Validates that a field value is in the allowed set.
form |> Depot.Gate.validate_inclusion("role", ["user", "admin", "mod"])Validates byte-length constraints on a field value.
Counts UTF-8 bytes, not Unicode characters. A 5-character Japanese string has 15 bytes, so LenMax(5) would reject it. Use this for binary-safe length limits; for character counts you must implement your own validator via validate_change.
Opts may be any combination of: LenMin(n) -- must have at least n bytes LenMax(n) -- must have at most n bytes LenExact(n) -- must be exactly n bytes LenMessage(m) -- (reserved; use add_error for custom messages)
Absent fields are silently skipped — use validate_required first.
form |> Depot.Gate.validate_length("name", [LenMin(2), LenMax(100)])Validates numeric constraints on a field (value must parse as an integer).
Opts may be any combination of: NumMin(n) -- >= n NumMax(n) -- <= n NumGreaterThan(n) -- > n NumLessThan(n) -- < n NumEqualTo(n) -- == n NumMessage(m) -- (reserved)
form |> Depot.Gate.validate_number("age", [NumMin(13), NumMax(150)])Validates that all listed fields have non-empty values.
form |> Depot.Gate.validate_required(["name", "email"])