defmodule Audit do
@moduledoc """
An implementation of Guy's value chain methodology for debugging complex functional programs
"""
@key :__audit_trail__
@enabled? Application.compile_env(:audit, :enabled?, false)
@type file_t :: binary()
@type line_t :: non_neg_integer()
@type trail_t :: {struct(), file_t(), line_t()}
@type change_t :: {struct(), trail_t()}
def start(_type, _args) do
Supervisor.start_link([Audit.FileCache], strategy: :one_for_one)
end
@dialyzer {:nowarn_function, audit_fun: 2}
@spec audit_fun(struct(), Macro.Env.t()) :: struct()
def audit_fun(r, e) do
r |> struct([{@key, payload(r, e)}])
end
@spec unaudit_fun(struct()) :: map()
def unaudit_fun(r) do
r |> Map.delete(@key)
end
@spec payload(struct(), Macro.Env) :: trail_t()
def payload(r, e), do: {r, e.file, e.line}
@spec record(trail_t()) :: struct
def record({r, _, _}), do: r
def record(_), do: nil
@spec file(trail_t()) :: file_t()
def file({_, f, _}), do: f
@spec line(trail_t()) :: line_t()
def line({_, _, l}), do: l
@spec trail(struct()) :: trail_t() | nil
def trail(struct), do: struct.__audit_trail__
@spec nth(struct(), non_neg_integer()) :: struct()
def nth(r, 0), do: r
def nth(r, n), do: nth(r |> trail |> record, n - 1)
@spec stringify_change(change_t()) :: binary()
defp stringify_change({post, {pre, filename, line}}) do
diff = Audit.Delta.delta(unaudit_fun(pre), unaudit_fun(post))
code = Audit.FileCache.get(filename) |> Enum.at(line - 1)
filename = String.replace_prefix(filename, "#{Audit.Github.git_root()}/", "")
url = Audit.Github.git_url(filename, line)
["github url: #{url}",
"local path: #{filename}:#{line}",
"code: #{String.trim(code)}",
"diff: #{inspect(diff)}"]
|> Enum.join("\n")
end
@spec change(term) :: change_t()
defp change(r = %_{__audit_trail__: audit_trail}) do
if audit_trail, do: {r, audit_trail}
end
@spec changelist(term) :: [change_t()]
defp changelist(r = %_{__audit_trail__: audit_trail}) do
if audit_trail, do: [{r, audit_trail} | changelist(record(audit_trail))], else: []
end
defp changelist(_), do: []
@spec to_string(struct) :: binary()
def to_string(r, last_only: true) do
r |> change() |> stringify_change()
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
if @enabled? do
defmacro audit(record) do
quote generated: true do
unquote(__MODULE__).audit_fun(unquote(record), __ENV__)
end
end
else
defmacro audit(record) do
quote generated: true do
unquote(record)
end
end
end
end