Packages
cqrs_tools
0.5.2
0.5.28
0.5.27
0.5.26
0.5.25
0.5.24
0.5.23
0.5.22
0.5.21
0.5.20
0.5.19
0.5.18
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
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.22
0.3.21
0.3.20
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
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.30
0.2.29
0.2.28
0.2.27
0.2.26
0.2.25
0.2.24
0.2.23
0.2.22
0.2.21
0.2.20
0.2.19
0.2.18
0.2.17
0.2.16
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.1
0.1.0
A collection of handy Elixir macros for CQRS applications.
Current section
Files
Jump to
Current section
Files
lib/cqrs/value_object.ex
defmodule Cqrs.ValueObject do
@moduledoc """
The `ValueObject` macro allows you to define a typed struct with validation.
## Options
* `require_all_fields` (:boolean) - If `true`, all fields will be required. Defaults to `true`
"""
@doc """
Allows one to define any custom data validation aside from casting and requiring fields.
This callback is optional.
Invoked when the `new()` or `new!()` function is called.
"""
@callback handle_validate(Ecto.Changeset.t(), keyword()) :: Ecto.Changeset.t()
@doc """
Allows one to modify the fully validated command. The changes to the command are validated again after this callback.
This callback is optional.
Invoked after the `handle_validate/2` callback is called.
"""
@callback after_validate(struct()) :: struct()
alias Cqrs.{Documentation, InvalidValuesError, ValueObject, ValueObjectError, Input}
defmacro __using__(opts \\ []) do
require_all_fields = Keyword.get(opts, :require_all_fields, true)
create_jason_encoders = Application.get_env(:cqrs_tools, :create_jason_encoders, true)
quote do
Module.put_attribute(__MODULE__, :require_all_fields, unquote(require_all_fields))
Module.put_attribute(__MODULE__, :create_jason_encoders, unquote(create_jason_encoders))
Module.register_attribute(__MODULE__, :schema_fields, accumulate: true)
Module.register_attribute(__MODULE__, :required_fields, accumulate: true)
import ValueObject, only: :macros
@desc nil
@behaviour ValueObject
@before_compile ValueObject
@impl true
def handle_validate(changeset, _opts), do: changeset
@impl true
def after_validate(object), do: object
defoverridable handle_validate: 2, after_validate: 1
end
end
defmacro __before_compile__(_env) do
quote do
ValueObject.__module_docs__()
ValueObject.__schema__()
ValueObject.__changeset__()
ValueObject.__introspection__()
ValueObject.__constructor__()
Module.delete_attribute(__MODULE__, :schema_fields)
Module.delete_attribute(__MODULE__, :required_fields)
Module.delete_attribute(__MODULE__, :require_all_fields)
Module.delete_attribute(__MODULE__, :create_jason_encoders)
end
end
defmacro __changeset__ do
quote do
def changeset(value_object \\ %__MODULE__{}, attrs)
def changeset(value_object, %__MODULE__{} = attrs),
do: changeset(value_object, Map.from_struct(attrs))
def changeset(value_object, %{} = attrs) do
fields = __MODULE__.__schema__(:fields)
value_object
|> Ecto.Changeset.cast(attrs, fields)
|> Ecto.Changeset.validate_required(@required_fields)
end
end
end
defmacro __schema__ do
quote do
use Ecto.Schema
if @create_jason_encoders and Code.ensure_loaded?(Jason), do: @derive(Jason.Encoder)
@primary_key false
embedded_schema do
Enum.map(@schema_fields, fn
{name, :enum, opts} ->
Ecto.Schema.field(name, Ecto.Enum, opts)
{name, :binary_id, opts} ->
Ecto.Schema.field(name, Ecto.UUID, opts)
{name, type, opts} ->
Ecto.Schema.field(name, type, opts)
end)
end
end
end
defmacro __introspection__ do
quote do
@name __MODULE__ |> Module.split() |> Enum.reverse() |> hd() |> to_string()
def __fields__, do: @schema_fields
def __required_fields__, do: @required_fields
def __module_docs__, do: @moduledoc
def __value_object__, do: __MODULE__
def __name__, do: @name
end
end
defmacro __module_docs__ do
quote do
require Documentation
moduledoc = @moduledoc || ""
@field_docs Documentation.field_docs("Fields", @schema_fields, @required_fields)
Module.put_attribute(
__MODULE__,
:moduledoc,
{1, moduledoc <> @field_docs}
)
end
end
defmacro __constructor__ do
quote do
@doc """
Creates a new `#{__MODULE__} object.`
#{@moduledoc}
"""
def new(attrs \\ [], opts \\ []) when is_list(opts),
do: ValueObject.__new__(__MODULE__, attrs, @required_fields, opts)
@doc """
Creates a new `#{__MODULE__} command.`
#{@moduledoc}
"""
def new!(attrs \\ [], opts \\ []) when is_list(opts),
do: ValueObject.__new__!(__MODULE__, attrs, @required_fields, opts)
end
end
@doc """
Defines a value object field.
* `:name` - any `atom`
* `:type` - any valid [Ecto Schema](`Ecto.Schema`) type
* `:opts` - any valid [Ecto Schema](`Ecto.Schema`) field options. Plus:
* `:required` - `true | false`. Defaults to the `require_all_fields` option.
* `:description` - Documentation for the field.
"""
@spec field(name :: atom(), type :: atom(), keyword()) :: any()
defmacro field(name, type, opts \\ []) do
quote location: :keep do
required = Keyword.get(unquote(opts), :required, @require_all_fields)
if required, do: @required_fields(unquote(name))
opts =
unquote(opts)
|> Keyword.put(:required, required)
|> Keyword.update(:description, @desc, &Function.identity/1)
# reset the @desc attr
@desc nil
@schema_fields {unquote(name), unquote(type), opts}
end
end
alias Ecto.Changeset
def __init__(mod, attrs, required_fields, opts) do
fields = mod.__schema__(:fields)
attrs = normalize(mod, attrs)
struct(mod)
|> Changeset.cast(attrs, fields)
|> Changeset.validate_required(required_fields)
|> mod.handle_validate(opts)
end
def __new__(mod, attrs, required_fields, opts) when is_list(opts) do
attrs = Input.normalize_input(attrs, mod)
mod
|> __init__(attrs, required_fields, opts)
|> case do
%{valid?: false} = changeset ->
{:error, changeset}
%{valid?: true} = changeset ->
attrs =
changeset
|> Changeset.apply_changes()
|> mod.after_validate()
|> Input.normalize_input(mod)
changeset2 = __init__(mod, attrs, required_fields, opts)
changeset
|> Changeset.merge(changeset2)
|> Changeset.apply_action(:create)
end
|> case do
{:ok, command} -> {:ok, command}
{:error, changeset} -> {:error, format_errors(changeset)}
end
end
def __new__!(mod, attrs, required_fields, opts \\ []) when is_list(opts) do
case __new__(mod, attrs, required_fields, opts) do
{:ok, command} -> command
{:error, errors} -> raise ValueObjectError, errors: errors
end
end
defp format_errors(changeset) do
Changeset.traverse_errors(changeset, fn {message, opts} ->
Regex.replace(~r"%{(\w+)}", message, fn _, key ->
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
end)
end)
end
defp normalize(_mod, values) when is_list(values), do: Enum.into(values, %{})
defp normalize(_mod, values) when is_struct(values), do: Map.from_struct(values)
defp normalize(_mod, values) when is_map(values), do: values
defp normalize(mod, _other), do: raise(InvalidValuesError, module: mod)
end