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_checker for email. The Configuration page lists them.

Step 1: Add the dependency

Add GuardedStruct to the deps of your mix.exs, then run mix deps.get.

elixir
def deps do
  [
    {:guarded_struct, "~> 0.1.4"}
  ]
end

If 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:

elixir
[
  import_deps: [:guarded_struct]
]

Step 2: Declare a struct

Create a module that uses GuardedStruct, and describe the fields inside a guardedstruct block.

elixir
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
end

Each field takes a name, a typespec and options. GuardedStruct uses the typespec for the generated @type t(). The options here are:

  • enforce: true makes the key required.
  • default: sets the value when the key is missing.
  • derives: holds a derive string. The sanitize ops run first and change the value. The validate ops 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.

elixir
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.

elixir
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.

elixir
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/1 returns {:ok, struct} or {:error, list}, and every error is a map with field, action and message.
  • The struct keeps the sanitized values, not the ones you passed in.
  • enforce: true makes a key required, and a missing key reports required_fields.

Next steps