defmodule Shake.Module do @moduledoc false @type step :: module | atom defmacro __using__(opts) do quote do @behaviour Shake @step_module_opts unquote(opts) def init(opts) do opts end def call(frame, opts) do step_module_call(frame, opts) end defoverridable [init: 1, call: 2] use Shake.Validate alias Shake.Frame import Shake.Frame import Shake.Module, only: [step: 1, step: 2] Module.register_attribute(__MODULE__, :steps, accumulate: true) @before_compile Shake.Module end end defmacro __before_compile__(env) do steps = Module.get_attribute(env.module, :steps) module_opts = Module.get_attribute(env.module, :step_module_opts) {frame, body} = Shake.Module.compile(env, steps, module_opts) quote do defp step_module_call(unquote(frame), _), do: unquote(body) end end defmacro step(step, opts \\ []) do quote do @steps {unquote(step), unquote(opts), true} end end def compile(env, job, module_opts) do frame = quote do: frame {frame, Enum.reduce(job, frame, "e_step(init_step(&1), &2, env, module_opts))} end # Initializes the options of a step at compile time. defp init_step({step, opts, guards}) do case Atom.to_char_list(step) do ~c"Elixir." ++ _ -> init_module_step(step, opts, guards) _ -> init_fun_step(step, opts, guards) end end defp init_module_step(step, opts, guards) do initialized_opts = step.init(opts) if function_exported?(step, :call, 2) do {:module, step, initialized_opts, guards} else raise ArgumentError, message: "#{inspect step} step must implement call/2" end end defp init_fun_step(step, opts, guards) do {:function, step, opts, guards} end # `acc` is a series of nested step calls in the form of # step3(step2(step1(frame))). `quote_step` wraps a new step around that series # of calls. defp quote_step({step_type, step, opts, guards}, acc, env, module_opts) do call = quote_step_call(step_type, step, opts) error_message = case step_type do :module -> "expected #{inspect step}.call/2 to return a Shake.Frame" :function -> "expected #{step}/2 to return a Shake.Frame" end <> ", all steps must receive a frame and return a frame" {fun, meta, [arg, [do: clauses]]} = quote do case unquote(compile_guards(call, guards)) do %Shake.Frame{halted: true} = frame -> unquote(log_halt(step_type, step, env, module_opts)) frame %Shake.Frame{} = frame -> unquote(acc) _ -> raise unquote(error_message) end end generated? = :erlang.system_info(:otp_release) >= '19' clauses = Enum.map(clauses, fn {:->, meta, args} -> if generated? do {:->, [generated: true] ++ meta, args} else {:->, Keyword.put(meta, :line, -1), args} end end) {fun, meta, [arg, [do: clauses]]} end defp quote_step_call(:function, step, opts) do quote do: unquote(step)(frame, unquote(Macro.escape(opts))) end defp quote_step_call(:module, step, opts) do quote do: unquote(step).call(frame, unquote(Macro.escape(opts))) end defp compile_guards(call, true) do call end defp compile_guards(call, guards) do quote do case true do true when unquote(guards) -> unquote(call) true -> frame end end end defp log_halt(step_type, step, env, module_opts) do if level = module_opts[:log_on_halt] do message = case step_type do :module -> "#{inspect env.module} halted in #{inspect step}.call/2" :function -> "#{inspect env.module} halted in #{inspect step}/2" end quote do require Logger # Matching, to make Dialyzer happy on code executing Shake.Builder.compile/3 _ = Logger.unquote(level)(unquote(message)) end else nil end end end