View Source Croma.Struct (croma v0.11.3)
Module to define structs with validation and conversion functions, based on its type information.
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.
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 generates defstruct
, @type t
and 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
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, :i}]}}
default-value-of-each-field
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
new-1
new/1
new/1
generated by Croma.Struct
deserves special attention.
new/1
can be useful when validating and converting data structure obtained from
e.g. JSON into an Elixir structs.
As with other type modules' new/1
, new/1
generated by Croma.Struct
can accept
value where valid?/1
evaluates to false
; it tries to convert that value into a
valid form.
Most notable example of this behaviour is that new/1
accepts not only atom-keyed
maps but also string-keyed maps, although structs are atom-keyed maps.
how-it-works
How it works
new/1
tries to get valid values for all struct fields from the given map or keyword list.
For each field of the struct, a value is computed as follows:
- If a value is not found for a field,
- If the field has default value, that default is taken.
- If a value is found for a field,
- If the module for the field exports
new/1
it is called with the found value. - If the module for the field does not export
new/1
, the found value is instead validated withvalid?/1
.
- If the module for the field exports
When no valid value can be obtained for any of the struct fields, new/1
returns an error.
Note that the usage of field modules' new/1
enables to construct arbitrarily nested
data structure in a recursive manner.
naming-convention-of-field-names-case-of-identifiers
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.
tips
Tips
to-distinguish-missing-field-and-explict-nil-in-new-1
To distinguish missing field and explict nil
in new/1
Simply using Croma.TypeGen.nilable/1
for struct fields, we cannot distinguish
whether the value is not given or nil
is explicitly given, as the resulting
struct contains nil
for both cases.
By adjusting default value for the field we can distinguish these case.
As an example suppose we have the following S
.
defmodule S do
use Croma.Struct, fields: [
field: {union([fixed(:unset), fixed(nil), Croma.Integer]), [default: :unset]},
]
end
Then we can distinguish these patterns.
S.new(%{}) == %S{field: :unset}
S.new(%{field: nil}) == %S{field: nil}
S.new(%{field: 1}) == %S{field: 1}
In this example we use Croma.TypeGen.union/1
and Croma.TypeGen.fixed/1
, but one can of course
define his/her own type module that accepts these 3 cases.
In a similar manner, by setting default value for field we can implement struct fields that
- accepts missing field but rejects explicit
nil
, or - accepts explicit
nil
but rejects missing field.