eal/util/impl_chooser.ex

# Copyright(c) 2015-2023 ACCESS CO., LTD. All rights reserved.

defmodule AntikytheraEal.ImplChooser do
  defmacro __using__(_) do
    config_key =
      Module.split(__CALLER__.module)
      |> List.last()
      |> Macro.underscore()
      # during compilation, safe to call `String.to_atom/1`
      |> String.to_atom()

    behaviour_module = Module.safe_concat(__CALLER__.module, "Behaviour")

    quote bind_quoted: [config_key: config_key, behaviour_module: behaviour_module] do
      @behaviour behaviour_module
      impl_module = AntikytheraEal.ImplChooser.extract_impl_module(config_key)

      behaviour_module.behaviour_info(:callbacks)
      |> Enum.each(fn {name, arity} ->
        vars = AntikytheraEal.ImplChooser.make_vars(arity, __MODULE__)
        @impl true
        def unquote(name)(unquote_splicing(vars)) do
          # Use `apply/3` to avoid xref warnings about unavailability of `impl_module`
          # (`impl_module` can be defined by a mix project invisible from `:antikythera`).
          apply(unquote(impl_module), unquote(name), unquote(vars))
        end
      end)
    end
  end

  def make_vars(n, module) do
    # Works even if `n == 0`
    Enum.drop(0..n, 1)
    |> Enum.map(fn i ->
      # during compilation, safe to generate atoms
      Macro.var(:"arg#{i}", module)
    end)
  end

  def extract_impl_module(config_key) do
    being_compiled_app = Mix.Project.config()[:app]
    Application.fetch_env!(being_compiled_app, :eal_impl_modules) |> Keyword.fetch!(config_key)
  end
end