croma v0.10.2 Croma.Struct View Source
Utility module to define structs and some helper functions.
Using this module requires to prepare type modules for all struct fields. Each of per-field type module is expected to provide the following members:
- required:
@type t
- required:
@spec valid?(term) :: boolean
- optional:
@spec default() :: t
- optional:
@spec new(term) :: Croma.Result.t(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 where keys are field names and values are type modules:
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(term) :: Croma.Result.t(t)
@spec new!(term) :: t
@spec update(t, Dict.t) :: Croma.Result.t(t)
@spec update!(t, Dict.t) :: t
The functions listed above are all overridable, so you can for example implement your own validation rule that spans multiple fields.
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 converted
by new/1
and new!/1
using :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.
Default value of each field
You can specify default value of each struct field by
- giving
:default
option in per-field options - defining
default/0
in the field’s type module (which is evaluated at compile-time)
If you specify both, (1) takes precedence over (2).
Additionally, you can tell Croma.Struct
not to use default/0
by specifying no_default?: true
.
If no default value is provided for a field, then the field must be explicitly filled when constructing a new struct.
As an example, suppose you have the following modules.
defmodule I do
use Croma.SubtypeOfInt, min: 0, default: 1
end
defmodule S do
use Croma.Struct, fields: [
a: Croma.Integer,
b: I,
c: {Croma.Integer, [default: 2]},
d: {I , [default: 3]},
e: {Croma.Integer, [no_default?: true]},
f: {I , [no_default?: true]},
]
end
Note that I
has default/0
whereas Croma.Integer
does not export default/0
.
Then,
a
,e
andf
have no default values- Default value of
b
is1
- Default value of
c
is2
- Default value of
d
is3
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 nil
as input).
Also, if a field has an invalid value, new/1
will be called with that value as input.