March Docs

Gate

Gate — changeset-style form validation for Bastion.

A Gate is an immutable value that tracks:

  • The proposed changes (permitted fields extracted from raw params)
  • Validation errors accumulated along the pipeline
  • The original record data (for cast_record / update flows)

Gates flow through a validation pipeline using the pipe operator:

fn registration_gate(params) do
  Gate.cast(params, ["email", "password"])
    |> Gate.validate_required(["email", "password"])
    |> Gate.validate_format("email", ".+@.+\\..+")
    |> Gate.validate_length("password", [LenMin(12), LenMax(72)])
end

Check validity and read errors:

if Gate.valid?(gate) do
  -- apply to DB
else
  render_form(conn, gate)  -- errors show in template
end

The gate is valid when it has no errors. A gate with errors is still a complete value — you pass it back to the template to display inline errors.

unique_constraint/3 is a marker only: the DB layer checks uniqueness on insert and merges errors back into the gate via Gate.add_error/3.

Types

typeLenConstraintLenConstraint = LenMin(Int) | LenMax(Int)#
typeNumConstraintNumConstraint = NumMin(Int) | NumMax(Int)#
typeConstraintOptConstraintOpt = ConstraintName(String) | ConstraintMessage(String)#

Functions

fnadd_erroradd_error(gate : Gate, field : String, message : String) : Gate#

Add a validation error for a field. Accumulated — calling multiple times for the same field adds multiple errors, but error_for/2 returns the first.

    Gate.add_error(gate, "email", "has already been taken")
fncastcast(params : List((String, String)), permitted : List(String)) : Gate#

Build a gate from raw params, keeping only the permitted fields.

`params` is a list of (key, value) string pairs — e.g., from
`Request.body_params/1`.  Only fields named in `permitted` are
kept as changes; all others are dropped.

    Gate.cast([("email", "[email protected]"), ("role", "admin")], ["email"])
    -- Gate with changes: [("email", "[email protected]")]
    -- "role" dropped — not in permitted list
fncast_recordcast_record(record : List((String, String)), params : List((String, String)), permitted : List(String)) : Gate#

Update an existing record's fields from params, keeping only permitted fields.

Same as cast/2 but stores the original record data so error messages and
the template can show existing values for unchanged fields.

    let gate = Gate.cast_record(user, params, ["email"])
    -- changes = only the permitted fields that were in params
    -- original_data = user's existing fields
fndelete_changedelete_change(gate : Gate, field : String) : Gate#

Remove a change from the gate.

    Gate.delete_change(gate, "password")
fnerror_forerror_for(gate : Gate, field : String) : Option(String)#

Returns the first error message for a field, or None.

    Gate.error_for(gate, "email")  -- Some("has invalid format") or None
fnerrorserrors(gate : Gate) : List((String, String))#

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

    Gate.errors(gate)
    -- [("email", "has invalid format"), ("password", "is too short ...")]
fnget_changeget_change(gate : Gate, field : String) : Option(String)#

Returns the proposed change value for a field, if present.

None if the field was not in the permitted list or not in the params.

    Gate.get_change(gate, "email")  -- Some("[email protected]") or None
fnget_fieldget_field(gate : Gate, field : String) : Option(String)#

Returns the value for a field: the change if present, else the original data.

Use this in templates to populate form fields — shows the user's input
on re-render, falling back to the original record value.

    Gate.get_field(gate, "email")
fnput_changeput_change(gate : Gate, field : String, value : String) : Gate#

Add or replace a change for a field.

Used in validation pipeline helpers (e.g., hash_password replaces
the raw "password" change with "hashed_password").

    Gate.put_change(gate, "hashed_password", hashed)
fnunique_constraintunique_constraint(gate : Gate, field : String, opts : List(ConstraintOpt)) : Gate#

Mark a field with a unique constraint that will be checked by the DB layer.

This is a no-op in the gate itself — it records the constraint metadata
in the errors list as a special sentinel so that the DB layer (Depot) can
map a uniqueness violation back to a field-level error message.

`opts` may include ConstraintName(str) and ConstraintMessage(str).

    Gate.unique_constraint(gate, "email",
      [ConstraintName("users_email_index"),
       ConstraintMessage("has already been taken")])
fnvalidvalid(gate : Gate) : Bool#

Returns true if the gate has no validation errors.

    if Gate.valid?(gate) do Depot.insert(db, schema, gate) end
fnvalidate_confirmationvalidate_confirmation(gate : Gate, field : String) : Gate#

Validate that field matches field_confirmation.

Looks for a parallel change key `field ++ "_confirmation"`.  If the
confirmation key is not present, no error is added (treat as not filled).

    Gate.validate_confirmation(gate, "password")
    -- checks "password" == "password_confirmation"
fnvalidate_formatvalidate_format(gate : Gate, field : String, pattern : String) : Gate#

Validate that a field value matches a regex pattern string.

    Gate.validate_format(gate, "email", ".+@.+\\..+")
fnvalidate_inclusionvalidate_inclusion(gate : Gate, field : String, values : List(String)) : Gate#

Validate that a field value is one of the allowed values.

    Gate.validate_inclusion(gate, "role", ["admin", "member", "viewer"])
fnvalidate_lengthvalidate_length(gate : Gate, field : String, constraints : List(LenConstraint)) : Gate#

Validate the byte length of a field value.

Accepts a list of LenMin(n) and/or LenMax(n) constraints.

    Gate.validate_length(gate, "password", [LenMin(12), LenMax(72)])
    Gate.validate_length(gate, "username", [LenMin(3), LenMax(20)])
fnvalidate_numbervalidate_number(gate : Gate, field : String, constraints : List(NumConstraint)) : Gate#

Validate that a numeric string field falls within bounds.

Parses the field value as an integer before comparing.

    Gate.validate_number(gate, "age", [NumMin(18), NumMax(120)])
fnvalidate_requiredvalidate_required(gate : Gate, fields : List(String)) : Gate#

Require that all listed fields are present and non-empty.

    Gate.validate_required(gate, ["email", "password"])