Validate input without building a struct

v0.1.4

You do not always need a whole struct. GuardedStruct.Validate runs the rules you already declared against one value, one field or a few fields. Use it to check a single form field as the user types, to validate the fields of a PATCH request, or to test a derive string without a module. This guide shows the three functions.

Before you begin

The examples use this module. Fields have derive strings, and parent_email depends on account_type through on:.

elixir
defmodule Person do
  use GuardedStruct

  guardedstruct do
    field :name, String.t(), enforce: true, derives: "sanitize(trim) validate(string, max_len=80)"
    field :age, integer(), derives: "validate(integer, min_len=0, max_len=120)"
    field :email, String.t(), derives: "sanitize(trim, downcase) validate(email_r)"
    field :account_type, String.t(), derives: "validate(enum=String[personal::business])"
    field :parent_email, String.t(), on: "root::account_type", derives: "validate(email_r)"
  end
end

Check a value against a derive string

Validate.run/2 takes a derive string and a value. No module is involved. It returns {:ok, value} with the sanitized value, or {:error, errors}. The errors use the field name :__value__.

elixir
alias GuardedStruct.Validate

Validate.run("sanitize(trim, downcase) validate(string, email_r)", "  [email protected] ")
# {:ok, "[email protected]"}

Validate.run("validate(integer, min_len=0)", -5)
# {:error,
#  [
#    %{
#      field: :__value__,
#      action: :min_len,
#      message: "The minimum number the __value__ field is 0 and you have sent less than this number of entries"
#    }
#  ]}

Validate.run("", "x")
# {:ok, "x"}

Validate.run("validate(max_len=foo)", "x")
# {:error,
#  [
#    %{
#      field: :__value__,
#      action: :bad_parameters,
#      message: "invalid parameter for `max_len` on field :__value__: expected non-negative integer, got \"foo\"."
#    }
#  ]}

The string is checked the way a module checks a derives: string when it compiles. A string the parser cannot read, such as validate(enum=Atom[a, b]), a group named something other than sanitize or validate, such as validte(string), a group with no closing ), and an operand of the wrong type, such as max_len=foo or equal=Integer::abc, return an error with the action bad_parameters. They are never skipped and never raise. An empty or blank string checks nothing.

Check one field of a module

Validate.field/3 and Validate.field/4 take the module, the field name and the value. They run the field's derive string and its validator:. An unknown field returns an unknown_field error.

elixir
Validate.field(Person, :email, "  [email protected] ")
# {:ok, "[email protected]"}

Validate.field(Person, :email, "nope")
# {:error, [%{field: :email, action: :email_r, message: "Incorrect email in the email field."}]}

Validate.field(Person, :zzz, "nope")
# {:error, [%{field: :zzz, action: :unknown_field, message: "field :zzz is not defined on Person"}]}

Provide the fields a rule depends on

By default field/4 honors the on: and domain: options. A field with on: fails with dependent_keys unless you supply the field it depends on in context:. Pass mode: :isolated to skip those rules and run only the derive string and the validator.

elixir
Validate.field(Person, :parent_email, "[email protected]")
# {:error, [%{field: :parent_email, action: :dependent_keys, message: "The required dependency for field parent_email has not been submitted.\nYou must have field account_type in your input\n"}]}

Validate.field(Person, :parent_email, "[email protected]", context: %{account_type: "personal"})
# {:ok, "[email protected]"}

Validate.field(Person, :parent_email, "[email protected]", mode: :isolated)
# {:ok, "[email protected]"}

Check some fields together

Validate.partial/2 takes a map and validates only the keys it contains. Missing fields are skipped, and the required-field check does not run. Cross-field rules resolve from the same map. String keys are accepted.

elixir
Validate.partial(Person, %{email: " [email protected] ", age: 30})
# {:ok, %{age: 30, email: "[email protected]"}}

Validate.partial(Person, %{email: "bad", age: -1})
# {:error,
#  [
#    %{field: :age, action: :min_len, message: "The minimum number the age field is 0 and you have sent less than this number of entries"},
#    %{field: :email, action: :email_r, message: "Incorrect email in the email field."}
#  ]}

Validate.partial(Person, %{})
# {:ok, %{}}

A value that is not a map returns {:error, [%{field: :__value__, action: :bad_parameters, message: "input must be a map"}]}.

Know what these functions skip

  • They do not build a struct, run auto, apply from or call main_validator.
  • Validate.field/3,4 and Validate.partial/2 use the derive extensions and the validate_derive and sanitize_derive modules of the module, as builder/1 does.
  • They do not check enforce. Use builder/1 when a missing key must be an error.
  • On a sub_field, field/3 returns the value you pass without building the nested struct.

Next steps