defmodule Audit do @moduledoc """ An implementation of Guy's value chain methodology for debugging complex functional programs """ @key :__audit_trail__ @audit? Application.compile_env(:audit, :active, false) @type file_t :: binary() @type line_t :: non_neg_integer() @type trail_t(t) :: {t, file_t(), line_t()} def start(_type, _args) do Supervisor.start_link([Audit.FileCache], strategy: :one_for_one) end @spec audit_fun(struct(), Macro.Env) :: struct() def audit_fun(r, e) do r |> Map.put(@key, payload(r, e)) end @spec unaudit_fun(struct()) :: struct() def unaudit_fun(r) do r |> Map.delete(@key) end @spec payload(t, Macro.Env) :: trail_t(t) when t: var def payload(r, e), do: {r, e.file, e.line} @spec record(trail_t(t)) :: t when t: var def record({r, _, _}), do: r @spec file(trail_t(term)) :: file_t() def file({_, f, _}), do: f @spec line(trail_t(term)) :: line_t() def line({_, _, l}), do: l def trail(struct), do: struct |> Map.get(@key) def nth(r, 0), do: r def nth(r, n), do: nth(r |> trail |> record, n - 1) defp stringify_change({post, {pre, filename, line}}) do diff = Audit.Delta.delta(pre |> unaudit_fun, post |> unaudit_fun) file = Audit.FileCache.get(filename) start = file |> Enum.drop(line - 6) code = start |> Enum.drop(5) |> List.first() ["#{filename}:#{line}\n", code, "diff: #{inspect(diff)}"] |> Enum.join() end defp changelist(r) do audit_trail = trail(r) if audit_trail, do: [{r, audit_trail} | changelist(record(audit_trail))], else: [] end def to_string(r) do r |> changelist() |> Enum.map_join("\n=====\n", &stringify_change/1) end defmacro __using__(_opts) do quote do import Audit end end defmacro audit_real(record) do quote generated: true do Audit.audit_fun(unquote(record), __ENV__) end end defmacro audit(record) do if @audit? do quote generated: true do unquote(__MODULE__).audit_fun(unquote(record), __ENV__) end else quote do unquote(record) end end end end