lib/form.ex

defmodule Needle.Form do
  @moduledoc """

  """

  # alias Ecto.Changeset
  alias Needle.{ULID, Util}

  defmacro __using__(options), do: using(__CALLER__.module, options)

  @must_be_in_module "Needle.Form may only be used inside a defmodule!"

  def using(nil, _options),
    do: raise(CompileError, description: @must_be_in_module)

  def using(module, options) do
    otp_app = Util.get_otp_app(options)
    config = Application.get_env(otp_app, module, [])
    Module.put_attribute(module, __MODULE__, options)
    pointers = emit_pointers(config ++ options)

    quote do
      use Ecto.Schema
      require Needle.Changesets
      use Exto
      import Needle.Form
      unquote_splicing(pointers)
    end
  end

  @must_use "You must use Needle.Form before calling form_schema/1"

  defmacro form_schema(do: body) do
    module = __CALLER__.module
    schema_check_attr(Module.get_attribute(module, __MODULE__), module, body)
  end

  @foreign_key_type ULID

  defp schema_check_attr(options, module, body) when is_list(options) do
    otp_app = Util.get_otp_app(options)

    foreign_key = Module.get_attribute(module, :foreign_key_type, @foreign_key_type)

    quote do
      @primary_key false
      @foreign_key_type unquote(foreign_key)
      embedded_schema do
        unquote(body)
        Exto.flex_schema(unquote(otp_app))
      end
    end
  end

  defp schema_check_attr(_, _, _), do: raise(ArgumentError, message: @must_use)

  # defines __pointers__
  defp emit_pointers(config) do
    otp_app = Keyword.fetch!(config, :otp_app)

    [
      Util.pointers_clause(:role, :form),
      Util.pointers_clause(:otp_app, otp_app)
    ]
  end
end