Building DSLs

Real-world DSLs: Ecto schemas/queries, Phoenix router, Plug.Builder, ExUnit, Absinthe, Ash.

39.1 __using__

defmodule MyApp.Schema do
  defmacro __using__(opts) do
    quote do
      import MyApp.Schema, only: [field: 2]
      Module.register_attribute(__MODULE__, :fields, accumulate: true)
      @table unquote(Keyword.fetch!(opts, :table))
      @before_compile MyApp.Schema
    end
  end

  defmacro field(name, type) do
    quote do: @fields {unquote(name), unquote(type)}
  end

  defmacro __before_compile__(_env) do
    quote do
      def __table__, do: @table
      def __fields__, do: Enum.reverse(@fields)
    end
  end
end

defmodule User do
  use MyApp.Schema, table: "users"
  field :name, :string
  field :age, :integer
end

User.__fields__()   # [name: :string, age: :integer]

39.2 A tiny test framework DSL

Real-world DSLs: Ecto schemas/queries, Phoenix router, Plug.Builder, ExUnit, Absinthe, Ash.

defmodule MiniTest do
  defmacro __using__(_) do
    quote do
      import MiniTest
      Module.register_attribute(__MODULE__, :tests, accumulate: true)
      @before_compile MiniTest
    end
  end

  defmacro test(desc, do: body) do
    fun = String.to_atom("test " <> desc)
    quote do
      @tests {unquote(fun), unquote(desc)}
      def unquote(fun)(), do: unquote(body)
    end
  end

  defmacro __before_compile__(_) do
    quote do
      def run do
        for {fun, desc} <- @tests do
          result = if apply(__MODULE__, fun, []), do: "PASS", else: "FAIL"
          IO.puts("#{result}: #{desc}")
        end
      end
    end
  end
end