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.
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
endLook up fields with Info
The GuardedStruct.Info functions take the module as their first argument.
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 optionA 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.
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:
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.
{: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)
# falseDiff.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.
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.
| Event | Measurements | Metadata |
|---|---|---|
[:guarded_struct, :builder, :start] | system_time | module |
[:guarded_struct, :builder, :stop] | duration, in native time units | module, result (:ok or :error), and error_count when the result is :error |
[:guarded_struct, :builder, :exception] | duration | module, kind, reason, stacktrace |
The exception event fires when the build raises, including the raise you ask for with builder(attrs, true).
:telemetry.attach_many(
"guarded-struct-builds",
[[:guarded_struct, :builder, :stop], [:guarded_struct, :builder, :exception]],
&MyApp.Telemetry.handle_event/4,
nil
)Next steps
- API reference: every function of
Info,Diffand the generated module. - Use virtual, dynamic and pattern-keyed fields: the field kinds that
Inforeports. - Handle and translate errors: what to do with a failed build.