Get Started
v0.1.4
GuardedStruct is an Elixir library that declares a struct together with the rules its data must follow. You list the fields once and say how each one is cleaned and checked. The library generates the struct, its type and a builder/1 function. builder/1 takes a map and returns {:ok, struct} or {:error, errors}. This tutorial installs version 0.1.4 and builds a User struct.
What you will build
A User module with four fields. The name is trimmed and capitalized. The email is trimmed, lower-cased and checked. The age must be between 0 and 120. The role defaults to user and accepts three values. You then call User.builder/1 with good input and with bad input and read the results.
Before you begin
- A Mix project on Elixir 1.17 or later.
- GuardedStruct depends on Spark, Splode, Telemetry and HtmlSanitizeEx, and Mix fetches them for you. A few validate ops need an optional package, such as
email_checkerforemail. The Configuration page lists them.
Step 1: Add the dependency
Add GuardedStruct to the deps of your mix.exs, then run mix deps.get.
def deps do
[
{:guarded_struct, "~> 0.1.4"}
]
endIf your project uses Igniter, mix igniter.install guarded_struct does this for you. It also adds a lint alias and an empty derive_extensions list to config/config.exs.
To keep mix format from adding parentheses to field and sub_field, import the formatter rules in .formatter.exs:
[
import_deps: [:guarded_struct]
]Step 2: Declare a struct
Create a module that uses GuardedStruct, and describe the fields inside a guardedstruct block.
defmodule User do
use GuardedStruct
guardedstruct do
field :name, String.t(),
enforce: true,
derives: "sanitize(trim, capitalize) validate(string, not_empty, max_len=80)"
field :email, String.t(),
enforce: true,
derives: "sanitize(trim, downcase) validate(email_r)"
field :age, integer(), derives: "validate(integer, min_len=0, max_len=120)"
field :role, String.t(),
default: "user",
derives: "validate(enum=String[admin::user::guest])"
end
endEach field takes a name, a typespec and options. GuardedStruct uses the typespec for the generated @type t(). The options here are:
enforce: truemakes the key required.default:sets the value when the key is missing.derives:holds a derive string. Thesanitizeops run first and change the value. Thevalidateops run next and check it. How derives work explains the string.
Step 3: Build a valid struct
Call builder/1 with a map. The keys can be atoms or strings, so a decoded JSON body works as it is.
User.builder(%{name: " alice ", email: "[email protected]", age: 30})
# {:ok, %User{name: "Alice", email: "[email protected]", age: 30, role: "user"}}
User.builder(%{"name" => "bob", "email" => "[email protected]"})
# {:ok, %User{name: "Bob", email: "[email protected]", age: nil, role: "user"}}The name and email in the first result are the sanitized values, and role holds its default. The age is nil in the second result. A field with no value is skipped by the derive strings.
Step 4: See the errors
Pass input that breaks the rules. The builder returns every problem it found as a list of maps. Each map has field, action and message.
User.builder(%{name: "x", email: "bad", age: -5})
# {:error,
# [
# %{field: :email, action: :email_r, message: "Incorrect email in the email field."},
# %{
# field: :age,
# action: :min_len,
# message: "The minimum number the age field is 0 and you have sent less than this number of entries"
# }
# ]}
User.builder(%{email: "[email protected]"})
# {:error, [%{field: :name, action: :required_fields, message: "Please submit required fields."}]}The action names the check that failed. The order of errors from different fields is not fixed, so do not rely on it.
What GuardedStruct generated
The module is now a normal Elixir struct with extra functions.
User.keys()
# [:name, :email, :age, :role]
User.enforce_keys()
# [:email, :name]
User.example()
# %User{name: "", email: "", age: 0, role: "user"}example/0 returns a sample struct. Other functions include __information__/0, which describes the module. The API reference lists them all.
What you learned
builder/1returns{:ok, struct}or{:error, list}, and every error is a map withfield,actionandmessage.- The struct keeps the sanitized values, not the ones you passed in.
enforce: truemakes a key required, and a missing key reportsrequired_fields.
Next steps
- Nest structs with sub_field: structs inside structs, and lists of them.
- Validate ops and Sanitize ops: everything you can write in a derive string.
- Handle and translate errors: read, raise and translate errors.
- Use GuardedStruct in an Ash resource: the same rules in an Ash action.
- LiveBook walkthrough: runnable examples in the GuardedStruct repository.