Packages
croma
0.1.11
0.13.0
0.12.0
0.11.3
0.11.2
0.11.1
0.11.0
0.10.2
0.10.1
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.3
0.7.2
0.7.1
0.7.0
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.1
0.5.0
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.11
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
Elixir macro utilities to make type-based programming easier
Current section
Files
Jump to
Current section
Files
lib/croma/struct.ex
defmodule Croma.Struct do
@moduledoc """
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 validate(term) :: Croma.Result.t(t)`
- optional: `@spec default :: t`
Some helpers for defining such per-field 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`.
To define a struct, `use` this module with a keyword list:
defmodule S do
use Croma.Struct, 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 new(Dict.t) :: Croma.Result.t(t)`
- `@spec new!(Dict.t) :: t`
- `@spec validate(term) :: Croma.Result.t(t)`
- `@spec validate!(term) :: 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 validate(i) when is_integer(i), do: {:ok, i}
...> def validate(_), do: {:error, {:invalid_value, [__MODULE__]}}
...> def default, do: 0
...> end
...> defmodule S do
...> use Croma.Struct, i: I
...> end
...> S.validate([i: 5])
...> {:ok, %S{i: 5}}
...> S.validate(%{i: "not_an_integer"})
...> {:error, {:invalid_value, [S, I]}}
...> {: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]}}
"""
import Croma.Defun
alias Croma.Result, as: R
@doc false
def field_type_pairs(fields) do
Enum.map(fields, fn {key, mod} ->
{key, quote do: unquote(mod).t}
end)
end
@doc false
def dict_fetch2(dict, key) when is_list(dict) do
Enum.find_value(dict, :error, fn {k, v} ->
if k == key || k == Atom.to_string(key), do: {:ok, v}
end)
end
def dict_fetch2(dict, key) when is_map(dict) do
case Map.fetch(dict, key) do
{:ok, _} = r -> r
:error -> Map.fetch(dict, Atom.to_string(key))
end
end
defmacro __using__(fields) do
%Macro.Env{module: module} = __CALLER__
quote context: Croma, bind_quoted: [module: module, fields: fields] do
@fields fields
defstruct Keyword.keys(@fields)
field_type_pairs = Croma.Struct.field_type_pairs(@fields)
@type t :: %unquote(module){unquote_splicing(field_type_pairs)}
@doc """
Creates a new instance of #{__MODULE__} by using the given `dict` and the default value of each field.
Returns `{:ok, valid_struct}` or `{:error, reason}`.
The values in the `dict` are validated by each field's `validate/1` function.
"""
defun new(dict: Dict.t) :: R.t(t) do
rs = Enum.map(@fields, fn {field, mod} ->
case Croma.Struct.dict_fetch2(dict, field) do
{:ok, v} -> mod.validate(v)
:error ->
try do
{:ok, mod.default}
rescue
_ -> {:error, {:value_missing, [mod]}}
end
end
|> R.map(&{field, &1})
end)
case R.sequence(rs) do
{:ok , kvs } -> {:ok, struct(__MODULE__, kvs)}
{:error, reason} -> {:error, R.ErrorReason.add_context(reason, __MODULE__)}
end
end
@doc """
A variant of `new/1` which returns `t` or raise if validation fails.
In other words, `new/1` followed by `Croma.Result.get!/1`.
"""
defun new!(dict: Dict.t) :: t do
new(dict) |> R.get!
end
@doc """
Checks that the given `dict` is valid or not by using each field's `validate/1` function.
Returns `{:ok, valid_struct}` or `{:error, reason}`.
"""
defun validate(dict: Dict.t) :: R.t(t) do
dict when is_list(dict) or is_map(dict) ->
kv_results = Enum.map(@fields, fn {field, mod} ->
case Croma.Struct.dict_fetch2(dict, field) do
{:ok, v} -> v
:error -> nil
end
|> mod.validate
|> R.map(&{field, &1})
end)
case R.sequence(kv_results) do
{:ok , kvs } -> {:ok, struct(__MODULE__, kvs)}
{:error, reason} -> {:error, R.ErrorReason.add_context(reason, __MODULE__)}
end
_ -> {:error, {:invalid_value, [__MODULE__]}}
end
@doc """
A variant of `validate/1` which returns `t` or raise if validation fails.
In other words, `validate/1` followed by `Croma.Result.get!/1`.
"""
defun validate!(dict: Dict.t) :: t do
validate(dict) |> R.get!
end
@doc """
Updates an existing instance of #{__MODULE__} with the given `dict`.
The values in the `dict` are validated by each field's `validate/1` function.
Returns `{:ok, valid_struct}` or `{:error, reason}`.
"""
defun update(s: t, dict: Dict.t) :: R.t(t) do
(%{__struct__: __MODULE__} = s, dict) when is_list(dict) or is_map(dict) ->
kv_results = Enum.map(@fields, fn {field, mod} ->
case Croma.Struct.dict_fetch2(dict, field) do
{:ok, v} -> mod.validate(v) |> R.map(&{field, &1})
:error -> nil
end
end)
|> Enum.reject(&is_nil/1)
case R.sequence(kv_results) do
{:ok , kvs } -> {:ok, struct(s, kvs)}
{:error, reason} -> {:error, R.ErrorReason.add_context(reason, __MODULE__)}
end
end
@doc """
A variant of `update/2` which returns `t` or raise if validation fails.
In other words, `update/2` followed by `Croma.Result.get!/1`.
"""
defun update!(s: t, dict: Dict.t) :: t do
update(s, dict) |> R.get!
end
end
end
end