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