View Source Estructura (estructura v1.5.0)
Estructura
is a set of extensions for Elixir structures,
such as Access
implementation, Enumerable
and Collectable
implementations, validations and test data generation via StreamData
.
Estructura
simplifies the following
Access
implementation for structsEnumerable
implementation for structs (as maps)Collectable
implementation for one of struct’s fields (asMapSet
does)StreamData
generation of structs for property-based testing
Use Options
use Estructura
accepts four keyword arguments.
access: true | false | :lazy
whether to generate theAccess
implementation, defaulttrue
; whentrue
or:lazy
, it also producesput/3
andget/3
methods to be used withcoercion
andvalidation
, when:lazy
, instances ofEstructura.Lazy
are understood as valuescoercion: boolean() | [key()]
whether to generate the bunch ofcoerce_×××/1
functions to be overwritten by implementations, defaultfalse
validation: boolean() | [key()]
whether to generate the bunch ofvalidate_×××/1
functions to be overwritten by implementations, defaultfalse
calculated: [{key(), formula}] when formula: binary() | Formulae.t() | (t() -> any())
the calculated fieldsenumerable: boolean()
whether to generate theEnumerable
porotocol implementation, defaultfalse
collectable: false | key()
whether to generate theCollectable
protocol implementation, defaultfalse
; if non-falsey atom is given, it must point to a struct field whereCollectable
would collect. Should be one oflist()
,map()
,MapSet.t()
,bitstribg()
generator: %{optional(key()) => Estructura.Config.generator()}
the instructions for the__generate__/{0,1}
functions that would produce the target structure values suitable for usage inStreamData
property testing; the generated__generator__/1
function is overwritable.
Please note, that setting coercion
and/or validation
to truthy values has effect
if and only if access
has been also set to true
.
Typical example of usage would be:
defmodule MyStruct do
use Estructura,
access: true,
coercion: [:foo], # requires `c:MyStruct.Coercible.coerce_foo/1` impl
validation: true, # requires `c:MyStruct.Validatable.validate_×××/1` impls
calculated: [foo: "length(bar)"], # requires `:formulae` dependency
enumerable: true,
collectable: :bar,
generator: [
foo: {StreamData, :integer},
bar: {StreamData, :list_of, [{StreamData, :string, [:alphanumeric]}]},
baz: {StreamData, :fixed_map,
[[key1: {StreamData, :integer}, key2: {StreamData, :integer}]]}
]
defstruct foo: 0, bar: [], baz: %{}
@impl MyStruct.Coercible
def coerce_foo(value) when is_integer(value), do: {:ok, value}
def coerce_foo(value) when is_float(value), do: {:ok, round(value)}
def coerce_foo(value) when is_binary(value) do
case Integer.parse(value) do
{value, ""} -> {:ok, value}
_ -> {:error, "#{value} is not a valid integer value"}
end
end
def coerce_foo(value),
do: {:error, "Cannot coerce value given for `foo` field (#{inspect(value)})"}
@impl MyStruct.Validatable
def validate_foo(value) when value >= 0, do: {:ok, value}
def validate_foo(_), do: {:error, ":foo must be positive"}
@impl MyStruct.Validatable
def validate_bar(value), do: {:ok, value}
@impl MyStruct.Validatable
def validate_baz(value), do: {:ok, value}
end
The above would allow the following to be done with the structure:
s = %MyStruct{}
put_in s, [:foo], "42"
#⇒ %MyStruct{foo: 42, bar: [], baz: %{}}
for i <- [1, 2, 3], into: s, do: i
#⇒ %MyStruct{foo: 0, bar: [1, 2, 3], baz: %{}}
Enum.map(s, &elem(&1, 1))
#⇒ [0, [], %{}]
MyStruct.__generator__() |> Enum.take(3)
#⇒ [
# %MyStruct{bar: [], baz: %{key1: 0, key2: 0}, foo: -1},
# %MyStruct{bar: ["g", "xO"], baz: %{key1: -1, key2: -2}, foo: 2},
# %MyStruct{bar: ["", "", ""], baz: %{key1: -3, key2: 1}, foo: -1}
# ]
Calculated fields
When using Access
, the calculated fields would be also updated upon the
update the fields then depend on.
Coercion
When coercion: true | [key()]
is passed as an argument to use Estructura
,
the nested behaviour Coercible
is generated and the target module claims to implement it.
To make a coercion work with MyStruct.put/3
and put_in/3
provided
by Access
implementation, the consumer module should implement MyStruct.Coercible
behaviour.
For the consumer convenience, the warnings for not implemented functions will be issued by compiler.
Validation
When validation: true | [key()]
is passed as an argument to use Estructura
,
the nested behaviour Validatable
is generated and the target module claims to implement it.
To make a validation work with MyStruct.put/3
and put_in/3
provided
by Access
implementation, the consumer module should implement MyStruct.Validatable
behaviour.
For the consumer convenience, the warnings for not implemented functions will be issued by compiler.
Generation
If generator
keyword argument has been passed, MyStruct.__generate__/{0,1}
can be
used to generate instances of this struct for StreamData
property based tests.
property "generation" do
check all %MyStruct{foo: foo, bar: bar, baz: baz} <- MyStruct.__generator__() do
assert match?(%{key1: v1, key2: v2} when is_integer(v1) and is_integer(v2), baz)
assert is_integer(foo)
assert is_binary(bar)
end
end
Lazy
If access: :lazy
is passed as an option, the struct content might be instantiated lazily,
upon first access through Kernel.×××_in/{2,3}
family.
This might be explicitly helpful when the real content requires a significant time to load and/or store. Consider the full response from the web server, including the gzipped content, which might in turn be a huge text file. Or an attachment to an email.
Instead of unarchiving the content, one might use Lazy
as
defmodule Response do
@moduledoc false
use Estructura, access: :lazy
def extract(file), do: {:ok, ZipHelper.unzip(file)}
defstruct __lazy_data__: nil,
file: Estructura.Lazy.new(&Response.extract/1)
end
response = %Response{__lazy_data__: zipped_content}
# immediate response
response |> get_in([:file])
# unzip and return
{unzipped, struct_with_cached_value} = response |> pop_in([:file])
# unzip and return the value, alter the struct with it
See Estructura.Lazy
for details and options, see Estructura.LazyMap
for
the implementation of lazy map.
Summary
Types
Diff return type
Types
@type diff_result() :: :diff | :overlap | :disjoint
Diff return type
Functions
Instantiates the struct by using Access
from a map, passing all coercions and validations.
@spec diff(map() | struct(), map() | struct(), :diff) :: {map(), map()}
@spec diff(map() | struct(), map() | struct(), :overlap | :disjoint) :: map()
Calculates the difference between two estructures and returns a tuple with the first element containing same values and the second one with diffs.
This function accepts maps but this options should be used as a last resort because structs are 4–6 times faster.
Examples
defmodule M do
use Estructura, enumerable: true
defstruct a: true, b: false
end
Estructura.diff(struct(M, []), struct(M, b: true), :diff)
#⇒{%{a: true}, %{b: [false, true]}}
Estructura.diff(%{a: true, b: false}, %{a: true, b: true}, :overlap)
#⇒ %{a: true}
Estructura.diff(%{a: true, b: false}, %{a: true, b: true}, :disjoint)
#⇒ %{b: [false, true]}