lib/exto.ex

defmodule Exto do
  @moduledoc "./README.md" |> File.stream!() |> Enum.drop(1) |> Enum.join()

  defmacro __using__(_options) do
    quote do
      import Exto, only: [flex_schema: 1]
      use Accessible
    end
  end

  @doc """
  Adds additional associations dynamically based on the config found under the name of the current module for the given OTP application. 
  
  Each key maps to an Ecto.Schema function:

  * `belongs_to`
  * `field`
  * `has_many`
  * `has_one`
  * `many_to_many`

  Each of these keys should map to a keyword list where the key is the name of the field or association and the value is one of:

  * A type
  * A tuple of type and options (keyword list)

  """
  defmacro flex_schema(otp_app) when is_atom(otp_app) do
    module = __CALLER__.module
    config = Application.get_env(otp_app, module, [])
    code = Enum.flat_map(config, &flex_category/1)

    quote do
      (unquote_splicing(code))
    end
  end

  # flex_schema impl

  @cats [:belongs_to, :field, :has_one, :has_many, :many_to_many]

  defp flex_category({:code, code}), do: [code]

  defp flex_category({cat, items}) when cat in @cats and is_list(items),
    do: Enum.map(items, &flex_association(cat, &1))

  # skip over anything else, they might use it!
  defp flex_category(_), do: []

  defp flex_association(rel, {name, type})
       when is_atom(name) and is_atom(type),
       do: flex_association(rel, name, type, [])

  defp flex_association(rel, {name, opts})
       when is_atom(name) and is_list(opts),
       do: flex_association(rel, name, opts)

  defp flex_association(rel, {name, {type, opts}})
       when is_atom(name) and is_atom(type) and is_list(opts),
       do: flex_association(rel, name, type, opts)

  defp flex_association(rel, {name, {opts}})
       when is_atom(name) and is_list(opts),
       do: flex_association(rel, name, opts)

  defp flex_association(rel, name, opts) do
    quote do
      unquote(rel)(unquote(name), unquote(opts))
    end
  end

  defp flex_association(rel, name, type, opts) do
    quote do
      unquote(rel)(unquote(name), unquote(type), unquote(opts))
    end
  end

end