lib/dispatchex.ex

defmodule DispatchEx do
  @moduledoc """

  Sets up a module for a protocol dispatch or a concrete implementation.

  ## Example

  ```
  defmodule PolymorphicCommand do
    use Dispatch, :protocol
    @callback cast(term()) :: {:ok, term()} | {:error, reason :: term()}

    # this becoms the default implementation
    def cast(_), do: raise ArgumentError, "unable to cast command description"
  end

  defmodule ReadCommand do
    use Dispatch, for: PolymorphicCommand
    def cast(%{type: read, key: k}), do: {:read, k}
  end

  defmodule WriteCommand do
    use Dispatch, for: PolymorphicCommand
    def cast(%{type: :write, key: k, value: v}), do: {:write, k, v}
  end

  {:read, 1} = PolymorphicCommand.cast(%{type: :read, key: 1})
  {:write, 1, :x} = PolymorphicCommand.cast(%{type: :write, key: 1, value: :x})
  ````
  """

  # Esnure Mix task is available
  require Mix.Tasks.Compile.Dispatch

  defmacro __using__(:protocol) do
    quote do
      Module.register_attribute(__MODULE__, :dispatch, persist: true)
      @dispatch __MODULE__
    end
  end

  defmacro __using__(for: proto) do
    proto = Macro.expand(proto, __CALLER__)

    quote do
      @moduledoc """
      Concrete implementation of #{unquote(proto)} protocol
      """
      @behaviour unquote(proto)

      Module.register_attribute(__MODULE__, :__impl__, persist: true, accumulate: true)

      require unquote(__MODULE__)

      import unquote(__MODULE__)
      import Kernel, except: [def: 1, def: 2]
    end
  end

  defmacro def(call, expr \\ nil) do
    call = Macro.expand(call, __CALLER__)

    quote location: :keep do
      @__impl__ unquote(Macro.escape(call))
      Kernel.def(unquote(call), unquote(expr))
    end
  end
end