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.

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

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

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

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

elixir
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