March Docs

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

ptypeDbConstraintDbConstraint = DbConstraint(String, String, String, String)#
typeGateGate = Gate(List((String, String)), List((String, String)), List((String, String)), Bool, List(DbConstraint))#
typeLengthOptLengthOpt =#
typeNumberOptNumberOpt =#
typeConstraintOptConstraintOpt =#

Functions

fncastcast(params, fields)#

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"])

fncast_recordcast_record(base, params, fields)#

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.

fnget_fieldget_field(form, field)#

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

fnget_changeget_change(form, field)#

Returns the change (cast value) for a field, or None if not present.

Depot.Gate.get_change(form, "email")

fnput_changeput_change(form, field, value)#

Sets a change value on the form.

form |> Depot.Gate.put_change("role", "admin")

fndelete_changedelete_change(form, field)#

Removes a change from the form (e.g. after hashing a password).

form |> Depot.Gate.delete_change("password")

fnbasebase(form)#

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)

fnchanged_fieldschanged_fields(form)#

Returns the list of field names that have changes.

Depot.Gate.changed_fields(gate) -- ["name", "email"]

fnchangedchanged(form, field)#

Returns true if the given field has a change recorded.

Depot.Gate.changed(gate, "name") -- true or false

fnapply_changesapply_changes(form)#

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"), ...]

fnadd_erroradd_error(form, field, message)#

Adds an error for a field and marks the form invalid.

form |> Depot.Gate.add_error("email", "has invalid format")

fnis_validis_valid(form)#

Returns true if the form has no validation errors.

if Depot.Gate.is_valid(form) then persist(form) else render_errors(form) end

fnerrorserrors(form)#

Returns all errors as a list of (field, message) pairs.

Depot.Gate.errors(form) -- [("email", "has invalid format"), ...]

fnget_errorsget_errors(form, field)#

Returns all error messages for a specific field.

Depot.Gate.get_errors(form, "email") -- ["has invalid format", ...]

fnvalidate_requiredvalidate_required(form, fields)#

Validates that all listed fields have non-empty values.

form |> Depot.Gate.validate_required(["name", "email"])

fnvalidate_lengthvalidate_length(form, field, opts)#

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)])

fnvalidate_formatvalidate_format(form, field, pattern)#

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.

fnvalidate_inclusionvalidate_inclusion(form, field, values)#

Validates that a field value is in the allowed set.

form |> Depot.Gate.validate_inclusion("role", ["user", "admin", "mod"])

fnvalidate_exclusionvalidate_exclusion(form, field, values)#

Validates that a field value is NOT in the excluded set.

form |> Depot.Gate.validate_exclusion("username", ["root", "admin"])

fnvalidate_numbervalidate_number(form, field, opts)#

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)])

fnvalidate_acceptancevalidate_acceptance(form, field)#

Validates that a field has been accepted (checkbox style: value "true" or "1").

form |> Depot.Gate.validate_acceptance("terms_of_service")

fnvalidate_confirmationvalidate_confirmation(form, field)#

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")

fnvalidate_changevalidate_change(form, field, validator)#

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 )

fnunique_constraintunique_constraint(form, field, opts)#

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")])

fnforeign_key_constraintforeign_key_constraint(form, field, opts)#

Registers a foreign key constraint hint.

form |> Depot.Gate.foreign_key_constraint("user_id", [ConstraintName("posts_user_id_fkey")])

fnno_assoc_constraintno_assoc_constraint(form, field, opts)#

Registers a no-association constraint hint.

form |> Depot.Gate.no_assoc_constraint("posts", [ConstraintName("posts_user_id_fkey")])

fncheck_constraintcheck_constraint(form, field, opts)#

Registers a check constraint hint.

form |> Depot.Gate.check_constraint("age", [ConstraintName("users_age_check"), ConstraintMessage("must be 13 or older")])

fnapply_constraint_errorapply_constraint_error(form, constraint_name)#

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")