Inspect, diff and observe structs

v0.1.4

A guarded module describes itself at run time. You can list its fields, ask what kind each one is, compare two built structs, produce a sample struct, and receive an event every time a struct is built. This guide covers GuardedStruct.Info, GuardedStruct.Diff, example/0 and the telemetry events.

Before you begin

The examples use this module. Section enforce: true makes every field without a default required. It does not reach inside a sub_field, so city says enforce: true itself.

elixir
defmodule User do
  use GuardedStruct

  guardedstruct enforce: true do
    field :name, String.t()
    field :email, String.t(), derives: "validate(email_r)"
    virtual_field :password_confirm, String.t()

    sub_field :address, struct() do
      field :city, String.t(), enforce: true
    end
  end
end

Look up fields with Info

The GuardedStruct.Info functions take the module as their first argument.

elixir
alias GuardedStruct.Info

Info.fields(User)                      # [:name, :email, :address, :password_confirm]
Info.field_kind(User, :address)        # :sub_field
Info.enforce?(User, :email)            # true
Info.virtual?(User, :password_confirm) # true
Info.field_derives(User, :email)       # "validate(email_r)"
Info.sub_fields(User)                  # [:address]
Info.virtual_fields(User)              # [:password_confirm]
Info.sub_module(User, :address)        # User.Address
Info.enforce?(User)                    # true, the section option

A field kind is one of :field, :sub_field, :conditional_field, :virtual_field, :dynamic_field or, for a regex-named field, :pattern_field. The API reference lists every function.

Dump everything with describe

Info.describe/1 returns one map with the module, its keys, its section options and the metadata of every field. It works on generated sub-modules too.

elixir
Info.describe(User) |> Map.take([:module, :path, :key, :shape, :keys, :enforce_keys])
# %{
#   module: User,
#   path: [],
#   key: :root,
#   shape: :struct,
#   keys: [:name, :email, :address],
#   enforce_keys: [:address, :email, :name]
# }

Info.describe(User).options
# %{
#   error: false, module: nil, json: false, opaque: false, enforce: true,
#   authorized_fields: false, main_validator: nil, sanitize_derive: nil, validate_derive: nil
# }

The keys list leaves out virtual fields because they are not in the struct. Each entry of fields carries the keys of the field metadata, plus enforce?, and sub_module for a sub_field. For nested key lists, call User.keys(:all) and User.enforce_keys(:all). The second lists only the required keys at every level, so :city shows because its field is enforce: true:

elixir
User.keys(:all)
# [:name, :email, %{address: [:city]}]

User.enforce_keys(:all)
# [%{address: [:city]}, :email, :name]

Compare two structs with Diff

GuardedStruct.Diff.diff/2 returns only what changed. A changed value is {:changed, old, new}. A nested struct that changed becomes a nested map of the same form. Equal fields are left out, and two values of different types return :not_comparable.

elixir
{:ok, v1} = User.builder(%{name: "Alice", email: "[email protected]", address: %{city: "NYC"}})
{:ok, v2} = User.builder(%{name: "Alicia", email: "[email protected]", address: %{city: "Boston"}})

GuardedStruct.Diff.diff(v1, v2)
# %{
#   name: {:changed, "Alice", "Alicia"},
#   address: %{city: {:changed, "NYC", "Boston"}}
# }

GuardedStruct.Diff.apply(v1, %{name: {:changed, "Alice", "Alicia"}})
# %User{name: "Alicia", email: "[email protected]", address: %User.Address{city: "NYC"}}

GuardedStruct.Diff.equal?(v1, v2)
# false

Diff.apply/2 sets the new value of every :changed entry and ignores keys the struct does not have.

Build a sample with example

Every module has example/0. It returns a struct filled with your defaults, and with a placeholder for common types where there is no default: an empty string for String.t(), 0 for integer(), and so on. Nested structs get their own sample.

elixir
User.example()
# %User{name: "", email: "", address: %User.Address{city: ""}}

Watch builds with telemetry

Each call to a public builder emits events through :telemetry. Nested builds do not emit, so one call produces one start and one stop.

EventMeasurementsMetadata
[:guarded_struct, :builder, :start]system_timemodule
[:guarded_struct, :builder, :stop]duration, in native time unitsmodule, result (:ok or :error), and error_count when the result is :error
[:guarded_struct, :builder, :exception]durationmodule, kind, reason, stacktrace

The exception event fires when the build raises, including the raise you ask for with builder(attrs, true).

elixir
:telemetry.attach_many(
  "guarded-struct-builds",
  [[:guarded_struct, :builder, :stop], [:guarded_struct, :builder, :exception]],
  &MyApp.Telemetry.handle_event/4,
  nil
)

Next steps