lib/formular/client/compiler.ex

defmodule Formular.Client.Compiler do
  @moduledoc """
  Compiler for making the string code into an Elixir module.

  Here we use Agent to prevent concurrent compilation.
  """

  alias Formular.Client.Config
  alias Formular.Client.PubSub

  use Agent
  require Logger

  def start_link(opts),
    do: Agent.start_link(fn -> nil end, Keyword.put_new(opts, :name, __MODULE__))

  def handle_new_code_revision({pid, ref}, name, code, config) do
    Agent.update(__MODULE__, fn _ ->
      with {mod, name, context} = formula_cfg <- Config.formula_config(config, name) do
        case config do
          %{compiler: {m, f, a}} ->
            apply(m, f, [{code, name, mod, context} | a])
            |> report_result({pid, ref}, formula_cfg, code)

          %{compiler: f} when is_function(f, 1) ->
            apply(f, [{code, name, mod, context}])
            |> report_result({pid, ref}, formula_cfg, code)
        end
      end

      nil
    end)
  end

  def compile({code, name, mod, context}) do
    Logger.info(["Recompiling formula: #{name} to module #{mod}. Code: #{inspect(code)}"])

    temporarily_disable_compilation_warning(fn ->
      do_compile(code, name, mod, context)
    end)
  end

  defp do_compile(code, _name, mod, context) do
    Formular.compile_to_module!(code, mod, context)
    :ok
  rescue
    e in CompileError ->
      {:error, e}
  end

  defp temporarily_disable_compilation_warning(f) do
    prev_mc_config = Code.get_compiler_option(:ignore_module_conflict)
    Code.put_compiler_option(:ignore_module_conflict, true)

    ret = f.()

    Code.put_compiler_option(:ignore_module_conflict, prev_mc_config)

    ret
  end

  defp report_result(:ok, {pid, ref}, {mod, name, context}, code) do
    broadcast_success(name, code, mod, context)
    send(pid, {ref, :ok})
  end

  defp report_result({:error, e}, {pid, ref}, {mod, name, context}, code) do
    broadcast_failure(e, name, code, mod, context)
    send(pid, {ref, {:error, e}})
  end

  defp broadcast_success(name, code, mod, context) do
    PubSub.dispatch({:compiled, name, code, mod: mod, context: context})
  end

  defp broadcast_failure(err, name, code, mod, context) do
    PubSub.dispatch({:compile_failed, name, err, code, mod: mod, context: context})
  end
end