croma v0.7.1 Croma.Struct View Source

Utility module to define structs and some helper functions.

Using this module requires to prepare modules that represent each struct field. Each of per-field module must provide the following members:

  • required: @type t
  • required: @spec valid?(term) :: boolean
  • optional: @spec default() :: t

Some helpers for defining such per-field type modules are available.

  • Wrappers of built-in types such as Croma.String, Croma.Integer, etc.
  • Utility modules such as Croma.SubtypeOfString to define “subtypes” of existing types.
  • Ad-hoc module generators defined in Croma.TypeGen.
  • This module, Croma.Struct itself for nested structs.

    • :recursive_new? option may come in handy when constructing a nested struct. See the section below.

To define a struct, use this module with a keyword list:

defmodule S do
  use Croma.Struct, fields: [field1_name: Field1Module, field2_name: Field2Module]
end

Then the above code is converted to defstruct along with @type t.

This module also generates the following functions.

  • @spec valid?(term) :: boolean
  • @spec new(Dict.t) :: Croma.Result.t(t)
  • @spec new!(Dict.t) :: t
  • @spec update(t, Dict.t) :: Croma.Result.t(t)
  • @spec update!(t, Dict.t) :: t

Examples

iex> defmodule I do
...>   @type t :: integer
...>   def valid?(i), do: is_integer(i)
...>   def default(), do: 0
...> end

...> defmodule S do
...>   use Croma.Struct, fields: [i: I]
...> end

...> S.new(%{i: 5})
{:ok, %S{i: 5}}

...> S.valid?(%S{i: "not_an_integer"})
false

...> {:ok, s} = S.new(%{})
{:ok, %S{i: 0}}

...> S.update(s, [i: 2])
{:ok, %S{i: 2}}

...> S.update(s, %{"i" => "not_an_integer"})
{:error, {:invalid_value, [S, I]}}

Naming convention of field names (case of identifiers)

When working with structured data (e.g. JSON) from systems with different naming conventions, it’s convenient to adjust the names to your favorite convention in this layer. You can specify the acceptable naming schemes of data structures to be validated by :accept_case option of use Croma.Struct.

  • nil (default): Accepts only the given field names.
  • :lower_camel: Accepts both the given field names and their lower camel variants.
  • :upper_camel: Accepts both the given field names and their upper camel variants.
  • :snake: Accepts both the given field names and their snake cased variants.
  • :capital: Accepts both the given field names and their variants where all characters are capital.

Nested struct and :recursive_new?

When you make an instance of nested struct defined using Croma.Struct, it’s convenient to recursively calling new/1 for each sub-structs, so that whole data structure can be generated by just one invocation of new/1 of the root struct.

:recursive_new? option can be set to true for such case.

iex> defmodule Leaf do
...>   use Croma.Struct, fields: [ns: Croma.TypeGen.nilable(Croma.String)]
...> end

...> defmodule Branch do
...>   use Croma.Struct, fields: [l: Leaf], recursive_new?: true
...> end

...> defmodule Root do
...>   use Croma.Struct, fields: [b: Branch], recursive_new?: true
...> end

...> Root.new(%{})
{:ok, %Root{b: %Branch{l: %Leaf{ns: nil}}}}

Note that if a field is missing, complementary functions will be called in order of default/0 then new/1 (with empty map as input).

Also, if a field has an invalid value, new/1 will be called with that value as input.

Limitation

  • If you want to validate your struct with a rule that spans multiple fields (e.g. f1 and f2 must be “both nil” or “both integer”), you have to manually define @type t, valid?/1, etc.