Use virtual, dynamic and pattern-keyed fields
v0.1.4
Three kinds of field cover input that a plain field does not fit. A virtual_field is checked but never stored, a dynamic_field keeps a free-form map exactly as sent, and a field named with a regular expression describes a map with no fixed keys. This guide shows each one.
Before you begin
You need a guarded module. Keys that come from untrusted input are the reason these fields exist, and How the builder works explains how GuardedStruct treats them.
Check a value without storing it
A virtual_field takes the same options as a field except struct, structs and priority. Its derive string runs, but the key is left out of the struct. A common use is a password confirmation that the main validator compares and then drops.
defmodule Signup do
use GuardedStruct
guardedstruct do
field :email, String.t(), enforce: true, derives: "validate(string, email_r)"
field :password, String.t(), enforce: true, derives: "validate(string, min_len=8)"
virtual_field :password_confirm, String.t(), derives: "validate(string)"
end
def main_validator(attrs) do
if attrs[:password] == attrs[:password_confirm] do
{:ok, attrs}
else
{:error, [%{field: :password_confirm, action: :match, message: "passwords don't match"}]}
end
end
end
Signup.builder(%{email: "[email protected]", password: "longpassword", password_confirm: "longpassword"})
# {:ok, %Signup{email: "[email protected]", password: "longpassword"}}
Signup.builder(%{email: "[email protected]", password: "longpassword", password_confirm: "other"})
# {:error, [%{field: :password_confirm, action: :match, message: "passwords don't match"}]}The main validator sees the virtual value. The virtual field's own derive string runs after the main validator and only when the main validator passed. Signup.keys() returns [:email, :password], so the virtual key is not part of the struct.
Keep a map as it was sent
A dynamic_field holds a map whose keys you do not control. The builder returns the map exactly as it came in. String keys stay strings, atom keys stay atoms, and nested maps are not touched, so no atoms are created from user input. A dynamic_field inside a sub_field does the same, at any depth. The defaults are the type map(), the default value %{} and the derive string validate(map).
defmodule Doc do
use GuardedStruct
guardedstruct do
field :id, String.t(), enforce: true
dynamic_field :metadata
end
end
Doc.builder(%{id: "x", metadata: %{"foo" => 1, :bar => 2, "baz" => %{"nested" => 3}}})
# {:ok, %Doc{id: "x", metadata: %{:bar => 2, "baz" => %{"nested" => 3}, "foo" => 1}}}
Doc.builder(%{id: "x"})
# {:ok, %Doc{id: "x", metadata: %{}}}
Doc.builder(%{id: "x", metadata: "no"})
# {:error, [%{field: :metadata, action: :map, message: "The metadata field must be map"}]}Read the keys with the same form they were sent in. If you convert them to atoms later, do it for a fixed list of keys that you wrote in source, never for every key of the map.
Describe a map with no fixed keys
Name a field with a regular expression instead of an atom. The module then describes a map: every key that matches the pattern is accepted, and every value is built or checked the same way. builder/1 returns a plain map with string keys, not a struct.
defmodule Shard do
use GuardedStruct
guardedstruct do
field :node, String.t(), enforce: true, derives: "sanitize(trim) validate(ipv4)"
end
end
defmodule Shards do
use GuardedStruct
guardedstruct do
field ~r/^shard_\d+$/, struct(), struct: Shard, derives: "validate(map, not_empty)"
end
end
Shards.builder(%{"shard_1" => %{node: "10.0.0.1"}, "shard_2" => %{node: " 10.0.0.2 "}})
# {:ok, %{"shard_1" => %Shard{node: "10.0.0.1"}, "shard_2" => %Shard{node: "10.0.0.2"}}}
Shards.builder(%{"shard_1" => %{node: "10.0.0.1"}, "bad" => %{node: "10.0.0.2"}})
# {:error,
# [%{field: :__map__, action: :key_pattern, key: "bad", message: "key \"bad\" does not match any declared pattern"}]}
Shards.builder(%{"shard_1" => %{node: "nope"}})
# {:error, [%{field: :node, action: :ipv4, key: "shard_1", message: "Invalid format in the node field"}]}These rules apply to a pattern-keyed module:
- You can declare several pattern fields, and a key matches the first pattern that fits.
- The derive string of the first pattern field that has one runs on the whole map, so
validate(map, not_empty)above rejects an empty map. - The value of a pattern field can be built with
struct:, checked withvalidator:, or left as it is. The plain formfield ~r/^X-[A-Z][A-Za-z0-9\-]*$/, String.t()accepts any value for a matching key. Avalidator:error has the field:__map__and the key. - Mixing atom fields and regex fields in one
guardedstructis a compile error. Put the regex field in its own module and refer to it withstruct:. GuardedStruct.Info.pattern_keyed?/1tells you which kind of module you have.Info.fields/1lists its patterns, andInfo.field_kind/2returns:pattern_fieldfor a name that a pattern matches.
Next steps
- DSL reference: the options each entity accepts.
- Inspect, diff and observe structs: the introspection helpers that report each field kind.
- Nest structs with sub_field: fixed nested shapes.