Fill and check fields from other fields
v0.1.4
Four field options read other parts of the input: auto fills a field from a function, from copies a value from another path, on requires another path to be present, and domain limits a field by the value at another path. This guide shows each one and the path syntax they share.
Before you begin
You need a guarded module. The paths in this guide reach into nested structs, which Nest structs with sub_field explains.
Fill a field with auto
Set auto: {Module, :function} to call Module.function() and store the result. Add a third element to pass arguments: a single value is the only argument, and a list is spread as several arguments.
defmodule Ids do
def generate, do: "id-1"
def prefixed(prefix), do: prefix <> "-2"
def joined(first, second), do: first <> "-" <> second
end
defmodule Order do
use GuardedStruct
guardedstruct do
field :id, String.t(), auto: {Ids, :generate}
field :ref, String.t(), auto: {Ids, :prefixed, "ord"}
field :code, String.t(), auto: {Ids, :joined, ["a", "b"]}
field :customer, String.t(), enforce: true
sub_field :shipping, struct() do
field :contact, String.t(), from: "root::customer"
field :note, String.t(), on: "root::customer"
field :city, String.t()
end
end
end
Order.builder(%{id: "mine", customer: "ann", shipping: %{city: "Rome"}})
# {:ok,
# %Order{
# id: "id-1",
# ref: "ord-2",
# code: "a-b",
# customer: "ann",
# shipping: %Order.Shipping{contact: "ann", note: nil, city: "Rome"}
# }}By default auto replaces a value you sent: the id: "mine" above became "id-1". The compiler warns when the function does not exist.
Copy a value with from
Set from: "root::customer" to copy the value found at that path into the field. When the path holds a value, it replaces whatever the field had. When the path is missing or nil, the field keeps its own value. In the example, shipping.contact became "ann".
Require another field with on
Set on: "root::provider" to require that path whenever the field has a value. If the path is missing or nil, the builder returns a dependent_keys error. A field that has no value is not checked, so combine on with enforce: true when the field itself is required.
defmodule Link do
use GuardedStruct
guardedstruct do
field :provider, String.t()
field :provider_path, String.t(), on: "root::provider"
end
end
Link.builder(%{provider_path: "https://github.com"})
# {:error,
# [
# %{
# field: :provider_path,
# action: :dependent_keys,
# message: "The required dependency for field provider_path has not been submitted.\nYou must have field provider in your input\n"
# }
# ]}
Link.builder(%{})
# {:ok, %Link{provider: nil, provider_path: nil}}Constrain a field with domain
A domain string lists rules about paths elsewhere in the input. The rules apply only when the field itself has a value. Separate several rules with ::.
defmodule Login do
use GuardedStruct
guardedstruct do
field :username, String.t(),
domain: "!auth.action=String[admin, user]::?auth.social=Atom[banned]"
sub_field :auth, struct() do
field :action, String.t()
field :social, atom()
end
end
end
Login.builder(%{username: "ann", auth: %{action: "admin"}})
# {:ok, %Login{username: "ann", auth: %Login.Auth{action: "admin", social: nil}}}
Login.builder(%{username: "ann", auth: %{action: "root"}})
# {:error,
# [
# %{
# field: :username,
# action: :domain_parameters,
# field_path: "auth.action",
# message: "Based on field username input you have to send authorized data"
# }
# ]}Each rule has three parts:
- A prefix:
!means the path is required, and?means it may be missing. - A path of keys joined with dots, read from the top of the input:
auth.action. - An operand after
=that the value at the path must match.
When a required path is missing, the message ends with and required key. The input you passed to the builder is what the rules read, so a value that auto or from sets does not count.
| Operand | Matches |
|---|---|
String[a, b] | One of the strings. |
Atom[a, b] | One of the atoms. |
Integer[1, 2] | One of the integers. |
Map[%{x: 1}, %{x: 2}] | One of the maps. |
Tuple[{:a, 1}, {:b, 2}] | One of the tuples. |
Equal[Atom>>name] | The one value. Write >> where the type separator :: would go. |
Either[string, enum>>Integer[1>>2>>3]] | Any of several derive ops. Write enum>> where a derive string has enum=, and >> where it has ::. |
Custom[My.Module, ok?] | A function that takes the value and returns true or false. Write the full module path, not an alias. |
Build in edit mode
Pass a tuple to builder/1 to choose how auto behaves. :add is the default. :edit keeps a value you sent and runs auto only for fields that are missing or nil, which suits an update of a stored record.
Order.builder({:root, %{id: "mine", customer: "ann"}, :edit})
# {:ok, %Order{id: "mine", ref: "ord-2", code: "a-b", customer: "ann", shipping: nil}}Write paths
A path for from and on is a list of keys joined with ::. Start it with root to read from the whole input. Without root the path starts at the map of the struct being built, so inside a sub_field it reaches that nested map. A domain path uses dots and always starts at the top of the input.
Next steps
- How the builder works: where
auto,domain,onandfromrun among the other steps. - DSL reference: the string options of every entity.
- Error shapes and actions:
dependent_keysanddomain_parameters.