defmodule OnePiece.Commanded.TypeProvider do
@moduledoc """
Implements `Commanded.EventStore.TypeProvider` behavior. Using macros to generate the behavior.
"""
alias OnePiece.Commanded.TypeProvider
alias OnePiece.Commanded.Helpers
defmacro __using__(opts \\ []) do
quote bind_quoted: [opts: opts] do
import TypeProvider, only: [register_type: 2, import_type_provider: 1]
@type_mapping_prefix Keyword.get(opts, :prefix, "")
@behaviour Commanded.EventStore.TypeProvider
@before_compile TypeProvider
Module.register_attribute(__MODULE__, :type_mapping, accumulate: true)
end
end
@doc """
Registers a mapping from an name string to an Elixir Module that defines a struct.
## Example
defmodule MyTypeProvider do
use OnePiece.Commanded.TypeProvider,
prefix: "accounts." # optional, adds the prefix to the type name
register_type "account_created", AccountCreated
end
"""
@spec register_type(name :: String.t(), struct_mod :: module()) :: Macro.t()
defmacro register_type(name, struct_mod) do
quote bind_quoted: [name: name, struct_mod: struct_mod] do
TypeProvider.__register_type__(
__MODULE__,
name,
struct_mod
)
end
end
@doc """
Imports all the types from another module defined by `OnePiece.Commanded.TypeProvider`.
## Example
defmodule UserTypeProvider do
use OnePiece.Commanded.TypeProvider
# ...
end
defmodule MyAppTypeProvider do
use OnePiece.Commanded.TypeProvider
import_type_provider UserTypeProvider
end
"""
@spec import_type_provider(provider_mod :: module()) :: Macro.t()
defmacro import_type_provider(provider_mod) do
quote bind_quoted: [provider_mod: provider_mod] do
TypeProvider.__import_type_provider__(
__MODULE__,
provider_mod
)
end
end
defmacro __before_compile__(_env) do
quote do
def __type_mapping__, do: @type_mapping
unquote(TypeProvider.__add_to_struct_funcs__())
unquote(TypeProvider.__add_to_string_funcs__())
end
end
def __add_to_string_funcs__() do
quote unquote: false do
@spec to_string(struct()) :: String.t() | no_return()
for {_, name, struct_mod} <- @type_mapping do
def to_string(%unquote(struct_mod){}) do
unquote(name)
end
end
def to_string(struct) do
raise ArgumentError,
"#{inspect(struct)} is not registered in the #{inspect(__MODULE__)} type provider"
end
end
end
def __add_to_struct_funcs__() do
quote unquote: false do
@spec to_struct(String.t()) :: struct() | no_return()
for {_, name, struct_mod} <- @type_mapping do
def to_struct(unquote(name)) do
struct(unquote(struct_mod))
end
end
def to_struct(name) do
raise ArgumentError,
"#{inspect(name)} is not registered in the #{inspect(__MODULE__)} type provider"
end
end
end
def __register_type__(mod, original_name, struct_mod) do
name = name_with_prefix(mod, original_name)
unless Helpers.defines_struct?(struct_mod) do
raise ArgumentError,
"#{inspect(name)} registration expected #{inspect(struct_mod)} to be a module that implements a struct"
end
case find_mapping_by_name(mod, name) do
nil ->
add_mapping(mod, name, struct_mod)
{found_mod, name, found_struct_mod} ->
raise ArgumentError,
"#{inspect(name)} already registered with #{inspect(found_struct_mod)} in #{inspect(found_mod)}"
end
end
def __import_type_provider__(mod, provider_mod) do
unless type_provider?(provider_mod) do
raise ArgumentError,
"#{inspect(mod)} import expected #{inspect(provider_mod)} module to be a #{inspect(TypeProvider)}"
end
for {_, name, struct_mod} <- provider_mod.__type_mapping__() do
case find_mapping_by_name(mod, name) do
nil ->
add_mapping(mod, name, struct_mod)
{registered_mod, _, found_struct_mod} ->
raise ArgumentError,
"failed to import types from #{inspect(provider_mod)} into #{inspect(mod)} because #{inspect(name)} already registered for #{inspect(found_struct_mod)} registered in #{inspect(registered_mod)}"
end
end
end
defp name_with_prefix(mod, name) do
Module.get_attribute(mod, :type_mapping_prefix) <> name
end
defp add_mapping(mod, name, struct_mod) do
Module.put_attribute(mod, :type_mapping, {mod, name, struct_mod})
end
defp find_mapping_by_name(mod, name) do
mod
|> Module.get_attribute(:type_mapping)
|> Enum.find(&is_mapping?(&1, name))
end
defp is_mapping?({_, current_name, _struct_mod}, name) do
current_name == name
end
defp type_provider?(mod) do
:functions
|> mod.__info__()
|> Keyword.get(:__type_mapping__)
|> Kernel.!=(nil)
end
end