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)) end
DB 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
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 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 None
Returns the change (cast value) for a field, or None if not present.
Depot.Gate.get_change(form, "email")
Sets a change value on the form.
form |> Depot.Gate.put_change("role", "admin")
Removes a change from the form (e.g. after hashing a password).
form |> Depot.Gate.delete_change("password")
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)
Returns the list of field names that have changes.
Depot.Gate.changed_fields(gate) -- ["name", "email"]
Returns true if the given field has a change recorded.
Depot.Gate.changed(gate, "name") -- true or false
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", "alice@test.com"), ...]
Adds an error for a field and marks the form invalid.
form |> Depot.Gate.add_error("email", "has invalid format")
Returns true if the form has no validation errors.
if Depot.Gate.is_valid(form) then persist(form) else render_errors(form) end
Returns all errors as a list of (field, message) pairs.
Depot.Gate.errors(form) -- [("email", "has invalid format"), ...]
Returns all error messages for a specific field.
Depot.Gate.get_errors(form, "email") -- ["has invalid format", ...]
Validates that all listed fields have non-empty values.
form |> Depot.Gate.validate_required(["name", "email"])
Validates string length constraints on a field.
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 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 that a field value is NOT in the excluded set.
form |> Depot.Gate.validate_exclusion("username", ["root", "admin"])
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 a field has been accepted (checkbox style: value "true" or "1").
form |> Depot.Gate.validate_acceptance("terms_of_service")
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")
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 then Some("too short") else None end )
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")])
Registers a foreign key constraint hint.
form |> Depot.Gate.foreign_key_constraint("user_id", [ConstraintName("posts_user_id_fkey")])
Registers a no-association constraint hint.
form |> Depot.Gate.no_assoc_constraint("posts", [ConstraintName("posts_user_id_fkey")])
Registers a check constraint hint.
form |> Depot.Gate.check_constraint("age", [ConstraintName("users_age_check"), ConstraintMessage("must be 13 or older")])
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")