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