Handle and translate errors
v0.1.4
Every failure from GuardedStruct comes back in one shape, a list of maps. This guide shows how to read that list, filter it, turn it into an exception, wrap it in Splode errors, and replace the message texts with your own.
Before you begin
You need a guarded module and a call that fails. The examples use a module with a required field:
defmodule Strict do
use GuardedStruct
guardedstruct error: true do
field :name, String.t(), enforce: true
end
endRead the error list
builder/1, Validate.run/2, Validate.field/3,4, Validate.partial/2 and __guarded_change__/1 all return {:error, list} on failure. The second element is always a list. Each entry is a map with three keys.
fieldis the field name, or:__root__when the problem is not about one field.actionnames the check that failed, such asrequired_fields,email_rorvalidator.messageis the text, which you can translate (see Translate messages below).
Strict.builder(%{})
# {:error,
# [%{field: :name, action: :required_fields, message: "Please submit required fields."}]}Errors from nested structs and conditional fields wrap their children in an errors list. Do not rely on the order of errors that belong to different fields. The Error shapes and actions page lists every shape and action.
Filter by action
A missing required field produces one error for each field, not one error that lists them. Filter on action to collect them.
{:error, errors} = Strict.builder(%{})
errors
|> Enum.filter(&match?(%{action: :required_fields}, &1))
|> Enum.map(& &1.field)
# [:name]Flatten nested errors
A nested error wraps its children, and a conditional error does too. Walk them with a function that follows errors and builds a path.
defmodule ErrorPaths do
def flatten(errors, path \\ [])
def flatten(errors, path) when is_list(errors), do: Enum.flat_map(errors, &flatten(&1, path))
def flatten(%{field: field, errors: children}, path), do: flatten(children, path ++ [field])
def flatten(%{field: field, action: action, message: message}, path),
do: [%{path: path ++ [field], action: action, message: message}]
end
# With Account from Nest structs with sub_field:
{:error, errors} = Account.builder(%{name: "Acme", owner: %{email: "bad"}})
ErrorPaths.flatten(errors)
# [%{path: [:owner, :email], action: :email_r, message: "Incorrect email in the email field."}]Raise an exception
Set error: true on the section, as Strict does. GuardedStruct then generates an exception module named Strict.Error. Pass true as the second argument to builder to raise it instead of returning the tuple. Without error: true the argument has no effect.
Strict.builder(%{}, true)
# ** (Strict.Error) There is at least one validation problem with your data:
# Term: nil
# Errors: [%{message: "Please submit required fields.", action: :required_fields, field: :name}]The exception struct has the fields errors and term. A sub_field accepts error: true too and gets its own Error module, such as Account.Owner.Error. Only the outermost call raises.
Wrap errors with Splode
GuardedStruct.Errors.from_tuple/1 converts the list into Splode errors. It accepts the list or the whole {:error, list} tuple. The result is a GuardedStruct.Errors.Invalid error whose errors list holds one GuardedStruct.Errors.Validation per entry, with the fields field, action, message, hint and child_errors.
{:error, errors} = Strict.builder(%{})
class = GuardedStruct.Errors.from_tuple(errors)
class.__struct__
# GuardedStruct.Errors.Invalid
Enum.map(class.errors, &{&1.field, &1.action, &1.message})
# [{:name, :required_fields, "Please submit required fields."}]Use the usual Splode functions on the result, such as GuardedStruct.Errors.traverse_errors/2. A conditionals error keeps its children in child_errors. A map that has no action, such as the wrapper around a nested struct's errors, becomes a GuardedStruct.Errors.Unknown error that carries the original map.
Translate messages
Every message text comes from a callback of GuardedStruct.Messages. Write your own module that uses it, override the callbacks you want to change, and point the config at it. The callbacks you do not override keep their default text.
defmodule MyApp.Messages do
use GuardedStruct.Messages
def required_fields(), do: "Merci de remplir les champs obligatoires."
def uuid(field), do: "Le champ #{field} n'est pas un UUID valide."
end# config/config.exs
config :guarded_struct, message_backend: MyApp.MessagesGuardedStruct reads message_backend when it compiles, so set it before the dependency compiles and recompile the dependency after you change it. To use Gettext, call gettext/1 inside your callbacks. The Configuration page lists the callbacks and the argument each one receives.
Next steps
- Error shapes and actions: every action and where it comes from.
- Configuration:
message_backendand the message callbacks. - Inspect, diff and observe structs: count failed builds with telemetry.