defmodule OnePiece.Commanded.ValueObject do
@moduledoc """
Defines "Value Object" modules.
"""
alias Ecto.Changeset
@doc """
Converts the module into an `Ecto.Schema`.
It derives from `Jason.Encoder` and also adds some factory functions to create
structs.
## Usage
defmodule MyValueObject do
use OnePiece.Commanded.ValueObject
embedded_schema do
field :title, :string
# ...
end
end
{:ok, my_value} = MyValueObject.new(%{title: "Hello, World!"})
"""
@spec __using__(opts :: []) :: any()
defmacro __using__(_opts \\ []) do
quote do
alias OnePiece.Commanded.ValueObject
use Ecto.Schema
@derive Jason.Encoder
@primary_key false
@before_compile OnePiece.Commanded.ValueObject
@doc """
Creates a `t:t/0`.
"""
@spec new(attrs :: map()) :: {:ok, %__MODULE__{}}
def new(attrs) do
ValueObject.__new__(__MODULE__, attrs)
end
@doc """
Creates a `t:t/0`.
"""
@spec new!(attrs :: map()) :: %__MODULE__{}
def new!(attrs) do
ValueObject.__new__!(__MODULE__, attrs)
end
@doc """
Returns an `t:Ecto.Changeset.t/0` for a given `t:t/0` command.
"""
@spec changeset(message :: %__MODULE__{}, attrs :: map()) :: Ecto.Changeset.t()
def changeset(message, attrs) do
ValueObject.__changeset__(message, attrs)
end
defoverridable new: 1, new!: 1, changeset: 2
end
end
defmacro __before_compile__(env) do
enforced_keys = get_enforced_keys(env)
quote unquote: false, bind_quoted: [enforced_keys: enforced_keys] do
def __enforced_keys__ do
unquote(enforced_keys)
end
for the_key <- enforced_keys do
def __enforced_keys__?(unquote(the_key)) do
true
end
end
def __enforced_keys__?(_) do
false
end
end
end
defp get_enforced_keys(env) do
enforce_keys = Module.get_attribute(env.module, :enforce_keys) || []
enforce_keys ++ get_primary_key_name(env)
end
defp get_primary_key_name(env) do
case Module.get_attribute(env.module, :primary_key) do
{field_name, _, _} -> [field_name]
_ -> []
end
end
def __new__(struct_module, attrs) do
struct_module
|> apply_changeset(attrs)
|> Changeset.apply_action(:new)
end
def __new__!(struct_module, attrs) do
struct_module
|> apply_changeset(attrs)
|> Changeset.apply_action!(:new)
end
def __changeset__(%struct_module{} = message, attrs) do
embeds = struct_module.__schema__(:embeds)
allowed = struct_module.__schema__(:fields) -- embeds
changeset =
message
|> Changeset.cast(attrs, allowed)
|> Changeset.validate_required(struct_module.__enforced_keys__() -- embeds)
Enum.reduce(embeds, changeset, fn field, changeset ->
Changeset.cast_embed(changeset, field, required: struct_module.__enforced_keys__?(field))
end)
end
defp apply_changeset(struct_module, attrs) do
struct(struct_module)
|> struct_module.changeset(attrs)
end
end