lib/makina.ex

defmodule Makina do
  alias Makina.Error

  import Makina.Helpers

  @moduledoc File.read!("priv/docs/makina.md")

  @doc false
  @spec __using__(Keyword.t()) :: Macro.t()
  defmacro __using__(options) do
    options =
      options
      |> parallel_composition(__CALLER__)
      |> imports(__CALLER__)

    state_options =
      Enum.filter(options, fn {k, _v} -> k in [:extends] end) ++ [specs: true, docs: true]

    invariant_options = Enum.filter(options, fn {k, _v} -> k in [:extends] end)

    command_options =
      Enum.filter(options, fn {k, _v} -> k in [:extends, :implemented_by] end) ++
        [specs: true, docs: true, defaults: true]

    debug = Application.get_env(:makina, :debug, [])

    quote do
      use Makina.State, unquote(state_options)
      use Makina.Invariant, unquote(invariant_options)
      use Makina.Command, unquote(command_options)
      use Makina.Behaviour
      import unquote(__MODULE__)
      unquote(debug)
      @before_compile unquote(__MODULE__)
    end
  end

  @spec __before_compile__(Macro.Env.t()) :: Macro.t()
  defmacro __before_compile__(_env) do
    module = __CALLER__.module
    userdocs = Module.get_attribute(module, :moduledoc)
    # options = Module.get_attribute(module, :options)

    cmds = Module.get_attribute(module, :commands)
    attrs = Module.get_attribute(module, :attributes)
    invs = Module.get_attribute(module, :invariants)

    docs =
      docs("model_module.md",
        name: module,
        userdocs: userdocs,
        cmds: cmds,
        attrs: attrs,
        invs: invs,
        # TODO
        extends: nil,
        composed: nil,
        imports: nil
      )

    quote do
      @moduledoc unquote(docs)
    end
  end

  @doc """

  This macro rewrites the given expression into a symbolic call.

  """
  @spec symbolic(Macro.t()) :: Macro.t()
  defmacro symbolic(expr = {{:., _, [_, _]}, _, _}), do: to_symbolic_call(expr)

  defmacro symbolic(expr) do
    "error in symbolic expression: #{Macro.to_string(expr)}"
    |> Error.throw_error(__CALLER__)
  end

  @spec to_symbolic_call(Macro.t()) :: Macro.t()
  defp to_symbolic_call({{:., _, [module, function]}, _, args}) do
    args =
      Enum.map(args, fn
        arg = {{:., _, [_, _]}, _, _} -> to_symbolic_call(arg)
        arg -> arg
      end)

    quote do
      {:call, unquote(module), unquote(function), unquote(args)}
    end
  end

  ##############################################################################
  # Helpers
  ##############################################################################

  defp parallel_composition(options, env) do
    if not is_nil(options[:extends]) and is_list(options[:extends]) do
      composed = :"#{env.module}.Composed"

      contents =
        quote do
          use Makina.Composition, extends: unquote(options[:extends])
        end

      Module.create(composed, contents, env)
      Keyword.delete(options, :extends) |> Keyword.put(:extends, composed)
    else
      options
    end
  end

  defp imports(options, env) do
    if not is_nil(options[:where]) or not is_nil(options[:hiding]) do
      imports = :"#{env.module}.Imports"

      args =
        [model: options[:extends]] ++ Enum.filter(options, fn {k, _v} -> k in [:where, :hiding] end)

      contents =
        quote do
          use Makina.Import, unquote(args)
        end

      Module.create(imports, contents, env)

      options
      |> Keyword.delete(:where)
      |> Keyword.delete(:hiding)
      |> Keyword.delete(:extends)
      |> Keyword.put(:extends, imports)
    else
      options
    end
  end
end