Normandy.Validate (normandy v2.2.0)

View Source

Provides changeset-style validation for schemas.

This module implements validation logic for Normandy schemas, allowing you to validate data before creating or updating structs.

Features

  • Required field validation
  • Type casting and validation
  • Custom validation functions
  • Error accumulation
  • Change tracking

Example

changeset =
  %User{}
  |> Validate.cast(params, [:name, :email, :age])
  |> Validate.validate_required([:name, :email])

if changeset.valid? do
  apply_changes(changeset)
else
  handle_errors(changeset.errors)
end

Summary

Functions

Applies the changes of a valid changeset and returns {:ok, data}.

Returns the data apply_action/2 returns in {:ok, data}.

Returns data with the changes put in, whether or not the changeset is valid.

Casts params into a changeset on data, a schema struct.

Returns the default :empty_values of cast/4.

Returns whether field has no value in changeset: the value from get_field/2 is nil or a string that is empty or only whitespace.

Returns the value of key: the change if there is one, else the value in data, else default (nil) when data has no such key.

Returns the errors of changeset as a map of field to a list of msg_func results.

Casts params into every field of the schema struct data and requires the fields declared required: true.

Validates the given parameter is true.

Runs validator on the change of field and adds the errors it returns.

Runs validator as validate_change/3 does, and first records {field, metadata} in validations/1.

Checks that the param field_confirmation equals the param field.

Adds an "is reserved" error when the change of field is in data, an enumerable.

Adds a "has invalid format" error when the change of field does not match format.

Adds an "is invalid" error when the change of field is not in data, an enumerable.

Adds an error when the length of the change of field is not as opts asks.

Adds an error when the change of field fails a comparison in opts.

Adds a "can't be blank" error for each of fields that field_missing?/2 finds missing, and marks fields as required.

Adds a "has an invalid entry" error when the change of field, a list, has an element that is not in data, an enumerable.

Returns the validations recorded on changeset as {field, metadata} pairs, newest first.

Types

action()

@type action() :: nil | :insert | :update | :delete | :replace | :ignore | atom()

data()

@type data() :: map()

empty_value()

@type empty_value() :: (term() -> boolean()) | binary() | list() | map() | tuple()

error()

@type error() :: {String.t(), Keyword.t()}

t()

@type t() :: t(Normandy.Schema.t() | map() | nil)

t(data_type)

@type t(data_type) :: %Normandy.Validate{
  action: action(),
  changes: %{optional(atom()) => term()},
  data: data_type,
  errors: [{atom(), error()}],
  params: %{optional(String.t()) => term()} | nil,
  required: [atom()],
  types: types(),
  valid?: boolean(),
  validations: [validation()]
}

traverse_result()

@type traverse_result() :: %{required(atom()) => [term()] | traverse_result()}

types()

@type types() :: %{required(atom()) => Normandy.Type.t()}

validation()

@type validation() :: {atom(), term()}

Functions

apply_action(changeset, action)

@spec apply_action(t(), action()) ::
  {:ok, Normandy.Schema.t() | data()} | {:error, t()}

Applies the changes of a valid changeset and returns {:ok, data}.

For an invalid changeset it returns {:error, changeset} with action set. For a schema struct the compute: fields are filled; a compute function that raises gives {:error, changeset} with a "could not be computed (Exception)" error on that field. Raises ArgumentError when action is not an atom.

apply_action!(changeset, action)

@spec apply_action!(t(), action()) :: Normandy.Schema.t() | data()

Returns the data apply_action/2 returns in {:ok, data}.

Raises Normandy.InvalidChangesetError when apply_action/2 returns {:error, changeset}.

apply_changes(changeset)

@spec apply_changes(t()) :: Normandy.Schema.t() | data()

Returns data with the changes put in, whether or not the changeset is valid.

Only fields in the changeset's types are put. For a schema struct, the compute: fields are filled. Use apply_action/2 to refuse an invalid changeset.

cast(data, params, permitted, opts \\ [])

@spec cast(
  Normandy.Schema.t(),
  %{required(binary()) => term()} | %{required(atom()) => term()} | :invalid,
  [atom()],
  Keyword.t()
) :: t()

Casts params into a changeset on data, a schema struct.

Each key of permitted (a list of atoms) is read from params (atom or string keys) and cast to its field's type. The result goes in changes unless it equals the current value. A value that is empty (see :empty_values) becomes the field's default. A value that does not cast adds an "is invalid" error and makes the changeset invalid. A permitted key absent from params is skipped. params of :invalid gives an invalid changeset with no changes.

Options

  • :empty_values - values treated as empty (default empty_values/0)
  • :force_changes - put a value in changes even when it equals the current one (default false)
  • :message - fn field, metadata -> message | nil end; a string replaces the error message of a failed cast, nil keeps "is invalid"

Raises Normandy.CastError when params is a struct, is not a map or mixes atom and string keys. Raises ArgumentError for a permitted key that is not an atom or not a field of data, or a :message that is not a 2-arity function.

Example

With User, a schema with :name and :email fields:

changeset = Validate.cast(%User{}, %{"name" => "Ana"}, [:name, :email])
changeset.changes
#=> %{name: "Ana"}

empty_values()

@spec empty_values() :: [empty_value()]

Returns the default :empty_values of cast/4.

It is a list of functions and values. A param is empty when a function returns true for it or when it equals a value. The default holds one function, which is true for a string that is empty or only whitespace.

field_missing?(changeset, field)

@spec field_missing?(t(), atom()) :: boolean()

Returns whether field has no value in changeset: the value from get_field/2 is nil or a string that is empty or only whitespace.

Raises ArgumentError when field is not an atom or not a field of the changeset.

get_field(validate, key, default \\ nil)

@spec get_field(t(), atom(), term()) :: term()

Returns the value of key: the change if there is one, else the value in data, else default (nil) when data has no such key.

traverse_errors(changeset, msg_func)

@spec traverse_errors(
  t(),
  (error() -> String.t()) | (t(), atom(), error() -> String.t())
) ::
  traverse_result()

Returns the errors of changeset as a map of field to a list of msg_func results.

msg_func is called for each {message, metadata} error with that tuple, or, as a 3-arity function, with the changeset, the field and that tuple. A changeset with no errors gives %{}.

Example

For a changeset whose :name is required and blank:

Validate.traverse_errors(changeset, fn {message, _metadata} -> message end)
#=> %{name: ["can't be blank"]}

validate(data, params)

@spec validate(
  Normandy.Schema.t(),
  %{required(binary()) => term()} | %{required(atom()) => term()}
) ::
  t()

Casts params into every field of the schema struct data and requires the fields declared required: true.

Example

changeset = Validate.validate(%Profile{}, %{"name" => "Ana"})
changeset.valid?

validate_acceptance(changeset, field, opts \\ [])

@spec validate_acceptance(t(), atom(), Keyword.t()) :: t()

Validates the given parameter is true.

Note this validation only checks the parameter itself is true, never the field in the schema. That's because acceptance parameters do not need to be persisted, as by definition they would always be stored as true.

Options

  • :message - the message on failure, defaults to "must be accepted". Can also be a {msg, opts} tuple, to provide additional options when using traverse_errors/2.

Examples

validate_acceptance(changeset, :terms_of_service)
validate_acceptance(changeset, :rules, message: "please accept rules")

validate_change(changeset, field, validator)

@spec validate_change(
  t(),
  atom(),
  (atom(), term() -> [{atom(), String.t()} | {atom(), error()}])
) :: t()

Runs validator on the change of field and adds the errors it returns.

validator is called with the field and its new value, only when the changeset has a change for it that is not nil, and returns a list of {field, message} or {field, {message, metadata}}. Any error makes the changeset invalid. The data's current value is not checked. Raises ArgumentError when field is not a field of the changeset.

Example

For a changeset with an :age field:

Validate.validate_change(changeset, :age, fn :age, age ->
  if age < 0, do: [age: "must not be negative"], else: []
end)

validate_change(changeset, field, metadata, validator)

@spec validate_change(
  t(),
  atom(),
  term(),
  (atom(), term() -> [{atom(), String.t()} | {atom(), error()}])
) :: t()

Runs validator as validate_change/3 does, and first records {field, metadata} in validations/1.

validate_confirmation(changeset, field, opts \\ [])

@spec validate_confirmation(t(), atom(), Keyword.t()) :: t()

Checks that the param field_confirmation equals the param field.

It reads changeset.params, so it needs a changeset made by cast/4 from a map. A mismatch adds a "does not match confirmation" error on :field_confirmation. A changeset with no params (nil) is returned unchanged.

Options

  • :required - also add a "can't be blank" error when the confirmation param is absent (default false)
  • :message - the message on failure; a {message, opts} tuple adds metadata

validate_exclusion(changeset, field, data, opts \\ [])

@spec validate_exclusion(t(), atom(), Enum.t(), Keyword.t()) :: t()

Adds an "is reserved" error when the change of field is in data, an enumerable.

Options

  • :message - the message on failure; a {message, opts} tuple adds metadata

validate_format(changeset, field, format, opts \\ [])

@spec validate_format(t(), atom(), Regex.t(), Keyword.t()) :: t()

Adds a "has invalid format" error when the change of field does not match format.

Raises ArgumentError when the change is not a string.

Options

  • :message - the message on failure; a {message, opts} tuple adds metadata

validate_inclusion(changeset, field, data, opts \\ [])

@spec validate_inclusion(t(), atom(), Enum.t(), Keyword.t()) :: t()

Adds an "is invalid" error when the change of field is not in data, an enumerable.

Options

  • :message - the message on failure; a {message, opts} tuple adds metadata

validate_length(changeset, field, opts)

@spec validate_length(t(), atom(), Keyword.t()) :: t()

Adds an error when the length of the change of field is not as opts asks.

The change is a string (counted in graphemes by default), a list or a map.

Options

  • :is - the length must be exactly this
  • :min - the length must be at least this
  • :max - the length must be at most this
  • :count - for a string, :graphemes (default), :codepoints or :bytes
  • :message - the message on failure; a {message, opts} tuple adds metadata

validate_number(changeset, field, opts)

@spec validate_number(t(), atom(), Keyword.t()) :: t()

Adds an error when the change of field fails a comparison in opts.

Options

  • :less_than, :greater_than, :less_than_or_equal_to, :greater_than_or_equal_to, :equal_to, :not_equal_to - the number to compare the change with
  • :message - the message on failure; a {message, opts} tuple adds metadata

Raises ArgumentError when the change or a compared number is not a number, or when opts has any other key.

validate_required(changeset, fields, opts \\ [])

@spec validate_required(t(), list() | atom(), Keyword.t()) :: t()

Adds a "can't be blank" error for each of fields that field_missing?/2 finds missing, and marks fields as required.

A field that already has an error is skipped, and each field with a new error is removed from changes. fields is an atom or a list of atoms.

Options

  • :message - the message on failure (default "can't be blank"); a {message, opts} tuple adds metadata for traverse_errors/2

Raises ArgumentError for a field that is not an atom or not a field of the changeset.

validate_subset(changeset, field, data, opts \\ [])

@spec validate_subset(t(), atom(), Enum.t(), Keyword.t()) :: t()

Adds a "has an invalid entry" error when the change of field, a list, has an element that is not in data, an enumerable.

Options

  • :message - the message on failure; a {message, opts} tuple adds metadata

validations(validate)

@spec validations(t()) :: [{atom(), term()}]

Returns the validations recorded on changeset as {field, metadata} pairs, newest first.

metadata is what the validation stored, such as {:format, regex}, {:length, opts} or {:number, opts}.