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