lib/ark/interface.ex

defmodule Ark.Interface do
  defmacro definterface(proto, do: block) do
    {:__block__, _, top_level} = block

    defs =
      Enum.filter(
        top_level,
        &(is_tuple(&1) and tuple_size(&1) == 3 and :def == elem(&1, 0))
      )

    # Generate implementation of the protocol that will throw
    defs_for_any_itself =
      Enum.map(defs, fn {:def, _meta, [{function, _meta2, args}]} ->
        args = Enum.map(args, fn _ -> {:_, [], nil} end)

        quote do
          def unquote(function)(unquote_splicing(args)) do
            raise "#{inspect(unquote(proto))} must be derived"
          end
        end
      end)

    [
      # Define the protocol
      quote location: :keep do
        defprotocol unquote(proto) do
          unquote(block)
        end
      end,

      # Define the implemementation for Any, containing the deriving macro
      quote location: :keep do
        defimpl unquote(proto), for: Any do
          defmacro __deriving__(_module, _struct, _options) do
            proto = unquote(proto)

            quote do
              require Ark.Interface
              Ark.Interface.auto_impl(unquote(proto))
            end
          end

          unquote(defs_for_any_itself)
        end
      end
    ]
  end

  defmacro auto_impl(proto) do
    module = __CALLER__.module

    quote location: :keep,
          bind_quoted: [
            proto: proto,
            module: module
          ] do
      defs = proto.__protocol__(:functions)

      defimpl proto do
        Enum.each(defs, fn {function, arity} when arity > 0 ->
          args = Enum.map(1..arity//1, fn i -> Macro.var(:"arg_#{i}", :auto_defimpl) end)

          def unquote(function)(unquote_splicing(args)) do
            unquote(module).unquote(function)(unquote_splicing(args))
          end
        end)
      end
    end
  end
end