defmodule Default.Behaviour do @moduledoc """ Creates a behaviour that carries its own default implementation. When used into a behaviour module, when that module in turn is used, all functions defined on it are given to the using module. This allows you to have concrete implementations of the behaviour's default functionality for testing, unlike cramming them all into a __using__ macro. When the behaviour is used all provided functions are correctly annotated with their `@impl` referring to it, and all made overridable. You can provide two options when using the resulting behaviour module: - `:docs` (default: true) will copy over the documentation for each function - `:inline` (default: false) will inline the default function's implementation, instead of just proxying to the implementation on the behaviour module, which is the standard behaviour. Example: ```elixir defmodule Custom.Behaviour do use Default.Behaviour @callback foo :: atom @doc "Computes foo, returns bar." def foo, do: :bar end defmodule Fizz do use Custom.Behaviour end defmodule Buzz do use Custom.Behaviour @doc "Computes foo, returns baz." def foo, do: :baz end Fizz.foo #=> :bar Buzz.foo #=> :baz ``` """ def __on_definition__(env, kind, name, params, guards, body) do doc = Module.get_attribute(env.module, :doc) Module.put_attribute(env.module, :__functions__, {doc, kind, name, params, guards, body}) end defmacro __before_compile__(_env) do quote do @doc false def __functions__, do: @__functions__ end end defmacro __using__(opts \\ []) do code = Keyword.get(opts, :do) quote do Module.register_attribute(__MODULE__, :__functions__, accumulate: true) @on_definition Default.Behaviour @before_compile Default.Behaviour defmacro __using__(opts \\ []) do docs = Keyword.get(opts, :docs, true) inline = Keyword.get(opts, :inline, false) defaults = for {doc, kind, name, params, guards, body} <- __MODULE__.__functions__ do info = %{module: __MODULE__, kind: kind, docs: docs, inline: inline} Default.Behaviour.compose_default(info, doc, name, params, guards, body) end [ quote(do: @behaviour __MODULE__), defaults, quote(do: defoverridable __MODULE__), unquote(code), ] |> List.flatten |> Enum.filter(&(&1)) end end end # Ignore macros def compose_default(%{kind: kind}, _doc, _name, _params, _guards, _body) when not kind in ~w[def defp]a, do: nil # If we are inlining, we may need any and all functions, private ones included def compose_default(%{inline: true} = info, doc, name, params, guards, body) do compose_definition(info, doc, name, params, guards, body) end # Otherwise we are only interested in public functions def compose_default(%{kind: :def, module: module} = info, doc, name, params, guards, _body) do delegate = compose_delegate(module, name, params) [ compose_module_attribute(:impl, module), compose_definition(info, doc, name, params, guards, delegate), ] end # Throw away anything else def compose_default(_info, _doc, _name, _params, _guards, _body), do: nil defp compose_delegate(module, name, params) do args = Enum.map(params, fn {:\\, _, [arg, _default]} -> arg arg -> arg end) compose_application(module, name, args) end defp compose_definition(info = %{kind: :def}, doc, name, params, guards, body) do [ compose_docs(info, doc), compose_function(:def, name, params, guards, body), ] end defp compose_definition(%{kind: :defp}, _doc, name, params, guards, body) do compose_function(:defp, name, params, guards, body) end defp compose_docs(%{docs: false} = info, _doc), do: compose_docs(info, false) defp compose_docs(_info, {_, doc}) when is_binary(doc) do compose_module_attribute(:doc, doc) end defp compose_docs(_info, _doc) do compose_module_attribute(:doc, false) end defp compose_function(:def, name, params, guards, body) do quote do def unquote(compose_definition(name, params, guards)) do unquote(body) end end end defp compose_function(:defp, name, params, guards, body) do quote do defp unquote(compose_definition(name, params, guards)) do unquote(body) end end end defp compose_definition(name, params, []) do compose_call(name, params) end defp compose_definition(name, params, guards) do Enum.reduce(guards, compose_call(name, params), fn guard, node -> {:when, [], [node, guard]} end) end defp compose_call(name, params) do {name, [], params} end defp compose_module_attribute(attribute, value) do {:@, [], [ {attribute, [], [value]} ]} end defp compose_application(module, function, args) do {:apply, [], [module, function, args]} end end