Skip to content

Commit 1fd4522

Browse files
author
Alex
authored
Merge pull request #10 from alexocode/reed/tracer-integration
Add Tracer integration for rule evaluation visualization
2 parents f9758e0 + aa990ae commit 1fd4522

34 files changed

Lines changed: 355 additions & 444 deletions

.github/workflows/ci.yml

Lines changed: 9 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -7,85 +7,12 @@ on:
77
- main
88

99
jobs:
10-
mix_test:
11-
name: mix test (Erlang/OTP ${{matrix.otp}} | Elixir ${{matrix.elixir}})
12-
runs-on: ubuntu-latest
13-
container: hexpm/elixir:${{ matrix.elixir }}-erlang-${{ matrix.otp }}-alpine-3.11.6
14-
strategy:
15-
fail-fast: false
16-
matrix:
17-
include:
18-
- elixir: 1.6.6
19-
otp: 19.3.6.13
20-
- elixir: 1.7.4
21-
otp: 19.3.6.13
22-
- elixir: 1.8.2
23-
otp: 20.3.8.26
24-
- elixir: 1.9.4
25-
otp: 20.3.8.26
26-
- elixir: 1.10.3
27-
otp: 21.3.8.16
28-
env:
29-
MIX_ENV: test
30-
steps:
31-
- uses: actions/checkout@v2.3.1
32-
33-
- name: Cache - deps/
34-
uses: actions/cache@v1
35-
with:
36-
path: deps/
37-
key: alpine-elixir-${{ matrix.elixir }}-otp-${{ matrix.otp }}-deps-${{ hashFiles('**/mix.lock') }}
38-
restore-keys: alpine-elixir-${{ matrix.elixir }}-otp-${{ matrix.otp }}-deps-
39-
40-
- name: Install Dependencies
41-
run: |
42-
mix local.rebar --force
43-
mix local.hex --force
44-
mix deps.get
45-
46-
- name: Cache - _build/
47-
uses: actions/cache@v1
48-
with:
49-
path: _build/
50-
key: alpine-elixir-${{ matrix.elixir }}-otp-${{ matrix.otp }}-build-${{ hashFiles('**/mix.lock') }}
51-
restore-keys: alpine-elixir-${{ matrix.elixir }}-otp-${{ matrix.otp }}-build-
52-
53-
- run: mix compile
54-
- name: Run tests
55-
run: |
56-
# coveralls.github requires git to be available
57-
apk add --no-cache git > /dev/null
58-
59-
mix coveralls.github
60-
env:
61-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
62-
63-
check_style:
64-
name: Check style
65-
runs-on: ubuntu-latest
66-
container: hexpm/elixir:1.10.3-erlang-22.1.8.1-alpine-3.11.6
67-
steps:
68-
- uses: actions/checkout@v2.3.1
69-
70-
- name: Cache - deps/
71-
uses: actions/cache@v1
72-
with:
73-
path: deps/
74-
key: alpine-deps-${{ hashFiles('**/mix.lock') }}
75-
restore-keys: alpine-deps-
76-
77-
- name: Install Dependencies
78-
run: |
79-
mix local.rebar --force
80-
mix local.hex --force
81-
mix deps.get
82-
83-
- name: Cache - _build/
84-
uses: actions/cache@v1
85-
with:
86-
path: _build/
87-
key: alpine-build-${{ hashFiles('**/mix.lock') }}
88-
restore-keys: alpine-build-
89-
90-
- run: mix format --check-formatted
91-
- run: mix credo
10+
ci:
11+
uses: systemic-engineer/ci/.github/workflows/elixir-matrix.yml@main
12+
with:
13+
matrix: |
14+
[
15+
{"elixir": "1.18", "otp": "27.0", "alpine": "3.20"},
16+
{"elixir": "1.17", "otp": "27.0", "alpine": "3.20"},
17+
{"elixir": "1.16", "otp": "26.2", "alpine": "3.20"}
18+
]

.github/workflows/release.yml

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,11 @@ on:
55
types: [published]
66

77
jobs:
8-
publish_to_hex:
8+
publish:
99
name: Publish to Hex.pm
10-
runs-on: ubuntu-latest
11-
container: hexpm/elixir:1.10.3-erlang-22.1.8.1-alpine-3.11.6
12-
steps:
13-
- uses: actions/checkout@v2.3.1
14-
- name: Install dependencies
15-
run: |
16-
mix local.rebar --force
17-
mix local.hex --force
18-
mix deps.get
19-
- run: mix hex.publish --yes
20-
env:
21-
HEX_API_KEY: ${{ secrets.HEX_API_KEY }}
10+
uses: systemic-engineer/ci/.github/workflows/hex-publish.yml@main
11+
with:
12+
elixir-version: "1.18"
13+
otp-version: "27.0"
14+
secrets:
15+
hex-api-key: ${{ secrets.HEX_API_KEY }}

Justfile

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# List all commands
2+
default:
3+
@just --list
4+
5+
# Run all tests
6+
test:
7+
mix test
8+
9+
# Run only tests affected by recent changes (faster)
10+
test-stale:
11+
mix test --stale
12+
13+
# Format code
14+
format:
15+
mix format
16+
17+
# Pre-commit gate: run by the global TDD commit-msg hook
18+
pre-commit: test-stale
19+
20+
# Pre-push gate: full test suite before pushing
21+
pre-push: test

config/config.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This file is responsible for configuring your application
22
# and its dependencies with the aid of the Mix.Config module.
3-
use Mix.Config
3+
import Config
44

55
# This configuration is loaded before any dependency and is restricted
66
# to this project. If another project depends on this project, this

lib/brex.ex

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,40 @@ defmodule Brex do
201201
@spec number_of_clauses(Types.rule()) :: non_neg_integer()
202202
defdelegate number_of_clauses(rule), to: Rule
203203

204+
@doc """
205+
Evaluates a rule and returns a `Tracer.t()` tree for visual inspection.
206+
207+
Equivalent to `evaluate/2` followed by `Brex.Trace.from_result/1`. Allows you
208+
to pass a list of rules which get linked calling `all/1`.
209+
210+
The resulting `Tracer` can be rendered as a color-coded tree via `inspect/2`.
211+
See `Brex.Trace` and `Tracer` for details on inspect options.
212+
213+
# Examples
214+
215+
iex> trace = Brex.trace(&is_list/1, [1, 2])
216+
iex> Tracer.ok?(trace)
217+
true
218+
219+
iex> trace = Brex.trace(Brex.all([&is_list/1, &Keyword.keyword?/1]), [a: 1])
220+
iex> output = inspect(trace, custom_options: [depth: :infinity], syntax_colors: [])
221+
iex> output =~ "Tracer<OK>"
222+
true
223+
iex> output =~ ":all"
224+
true
225+
226+
iex> trace = Brex.trace(Brex.any([&is_list/1, &is_map/1]), "hello")
227+
iex> output = inspect(trace, custom_options: [depth: :error], syntax_colors: [])
228+
iex> output =~ "Tracer<ERROR>"
229+
true
230+
"""
231+
@spec trace(one_or_many_rules(), value()) :: Tracer.t()
232+
def trace(rules, value) do
233+
rules
234+
|> evaluate(value)
235+
|> Brex.Trace.from_result()
236+
end
237+
204238
operator_doc = fn operator ->
205239
"""
206240
Links the given rules in a boolean fashion, similar to the `Enum` functions.

lib/brex/result/formatter/rules.ex

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@ defmodule Brex.Result.Formatter.Rules do
3333
3434
iex> Brex.Result.Formatter.Rules.format([:foo])
3535
** (ArgumentError) Invalid result! Expected a list of or single `Brex.Result` struct but received: [:foo]
36-
(brex) lib/brex/result/formatter.ex:45: Brex.Result.Formatter.invalid_result!/1
37-
(elixir) lib/enum.ex:1294: Enum."-map/2-lists^map/1-0-"/2
3836
"""
3937

4038
use Brex.Result.Formatter

lib/brex/trace.ex

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
defmodule Brex.Trace do
2+
@moduledoc """
3+
Converts a `Brex.Result` evaluation tree into a `Tracer.t()` for visual
4+
inspection.
5+
6+
Each result becomes a node in the trace tree:
7+
8+
- `step` — the rule identity:
9+
* operators → `:all`, `:any`, `:none` (or the aggregator's function name)
10+
* named functions → `{module, name, arity}` MFA tuple
11+
* anonymous functions → `:anonymous_fn`
12+
* struct-based rules → the struct module
13+
* module-based rules → the module atom
14+
- `input` — the evaluated value
15+
- `output` — `{:ok, :passed}` / `{:error, :failed}` for operators; raw
16+
evaluation (`true`, `false`, `:ok`, `{:error, reason}`, etc.) for leaf rules
17+
- `nested` — child traces for operator rules; `[]` for leaf rules
18+
19+
Use `inspect/2` on the returned `Tracer.t()` to render a color-coded tree.
20+
See `Tracer` inspect options (`depth`, `indent`) for controlling output depth.
21+
22+
## Examples
23+
24+
iex> result = Brex.evaluate(&is_list/1, [1, 2])
25+
iex> trace = Brex.Trace.from_result(result)
26+
iex> trace.step
27+
{:erlang, :is_list, 1}
28+
iex> trace.input
29+
[1, 2]
30+
iex> trace.output
31+
true
32+
33+
iex> result = Brex.evaluate(&is_list/1, "not a list")
34+
iex> Brex.Trace.from_result(result) |> Tracer.error?()
35+
true
36+
37+
iex> result = Brex.evaluate(Brex.all([&is_list/1, &Keyword.keyword?/1]), [a: 1])
38+
iex> trace = Brex.Trace.from_result(result)
39+
iex> trace.step
40+
:all
41+
iex> trace.output
42+
{:ok, :passed}
43+
iex> length(trace.nested)
44+
2
45+
46+
iex> result = Brex.evaluate(Brex.any([&is_list/1, &is_map/1]), "hello")
47+
iex> trace = Brex.Trace.from_result(result)
48+
iex> trace.output
49+
{:error, :failed}
50+
iex> length(trace.nested)
51+
2
52+
53+
iex> result = Brex.evaluate(Brex.none([&is_list/1, &is_map/1]), "hello")
54+
iex> trace = Brex.Trace.from_result(result)
55+
iex> trace.step
56+
:none
57+
iex> Tracer.ok?(trace)
58+
true
59+
60+
iex> double = fn x -> x * 2 end
61+
iex> result = Brex.evaluate(double, 3)
62+
iex> Brex.Trace.from_result(result).step
63+
:anonymous_fn
64+
"""
65+
66+
@spec from_result(Brex.Result.t()) :: Tracer.t()
67+
def from_result(%Brex.Result{rule: %Brex.Operator{} = rule, value: value, evaluation: evaluation}) do
68+
{output, nested} =
69+
case evaluation do
70+
{:ok, results} -> {{:ok, :passed}, Enum.map(results, &from_result/1)}
71+
{:error, results} -> {{:error, :failed}, Enum.map(results, &from_result/1)}
72+
end
73+
74+
Tracer.new(step(rule), value, output, nested)
75+
end
76+
77+
def from_result(%Brex.Result{rule: rule, value: value, evaluation: evaluation}) do
78+
Tracer.new(step(rule), value, evaluation, [])
79+
end
80+
81+
defp step(%Brex.Operator{aggregator: agg}) do
82+
case Function.info(agg, :name) do
83+
{:name, :all?} -> :all
84+
{:name, :any?} -> :any
85+
{:name, :none?} -> :none
86+
{:name, name} -> name
87+
end
88+
end
89+
90+
defp step(fun) when is_function(fun) do
91+
info = Function.info(fun)
92+
93+
case info[:type] do
94+
:external -> {info[:module], info[:name], info[:arity]}
95+
:local -> :anonymous_fn
96+
end
97+
end
98+
99+
defp step(struct) when is_struct(struct), do: struct.__struct__
100+
defp step(other), do: other
101+
end

mix.exs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ defmodule Brex.Mixfile do
99
elixir: "~> 1.5",
1010
elixirc_paths: elixirc_paths(Mix.env()),
1111
preferred_cli_env: [
12-
espec: :test,
1312
coveralls: :test,
1413
"coveralls.detail": :test,
1514
"coveralls.post": :test,
@@ -40,26 +39,27 @@ defmodule Brex.Mixfile do
4039
end
4140

4241
# Specifies which paths to compile per environment.
43-
defp elixirc_paths(:test), do: ["lib", "spec/support"]
42+
defp elixirc_paths(:test), do: ["lib", "test/support"]
4443
defp elixirc_paths(_), do: ["lib"]
4544

4645
# Run "mix help deps" to learn about dependencies.
4746
defp deps do
4847
[
48+
# Runtime
49+
{:tracer, github: "systemic-engineer/tracer"},
50+
4951
# No Runtime
5052
{:ex_doc, "~> 0.19", only: :dev, runtime: false},
5153

5254
# Test
5355
{:credo, "~> 1.4", only: :dev, runtime: false},
5456
{:excoveralls, "~> 0.13", only: :test},
55-
{:espec, "~> 1.6", only: :test}
57+
{:ssl_verify_fun, "~> 1.1.7", only: :test, override: true}
5658
]
5759
end
5860

5961
def aliases do
60-
[
61-
test: "espec"
62-
]
62+
[]
6363
end
6464

6565
def description do

mix.lock

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
"certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"},
44
"credo": {:hex, :credo, "1.4.1", "16392f1edd2cdb1de9fe4004f5ab0ae612c92e230433968eab00aafd976282fc", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "155f8a2989ad77504de5d8291fa0d41320fdcaa6a1030472e9967f285f8c7692"},
55
"earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm", "b42a23e9bd92d65d16db2f75553982e58519054095356a418bb8320bbacb58b1"},
6-
"espec": {:hex, :espec, "1.6.3", "d9355788e508b82743a1b1b9aa5ac64ba37b0547c6210328d909e8a6eb56d42e", [:mix], [{:meck, "0.8.12", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "235ef9931fc6ae8066272b77dc11c462e72af0aa50c6023643acd22b09326d21"},
76
"ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "dc87f778d8260da0189a622f62790f6202af72f2f3dee6e78d91a18dd2fcd137"},
87
"excoveralls": {:hex, :excoveralls, "0.13.3", "edc5f69218f84c2bf61b3609a22ddf1cec0fbf7d1ba79e59f4c16d42ea4347ed", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cc26f48d2f68666380b83d8aafda0fffc65dafcc8d8650358e0b61f6a99b1154"},
98
"exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"},
@@ -13,11 +12,11 @@
1312
"jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm"},
1413
"makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d7152ff93f2eac07905f510dfa03397134345ba4673a00fbf7119bab98632940"},
1514
"makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "4a36dd2d0d5c5f98d95b3f410d7071cd661d5af310472229dd0e92161f168a44"},
16-
"meck": {:hex, :meck, "0.8.12", "1f7b1a9f5d12c511848fec26bbefd09a21e1432eadb8982d9a8aceb9891a3cf2", [:rebar3], [], "hexpm", "7a6ab35a42e6c846636e8ecd6fdf2cc2e3f09dbee1abb15c1a7c705c10775787"},
1715
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
1816
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
1917
"nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm", "ebb595e19456a72786db6dcd370d320350cb624f0b6203fcc7e23161d49b0ffb"},
2018
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
21-
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
19+
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
20+
"tracer": {:git, "https://github.com/systemic-engineer/tracer.git", "8f3bcc3a051f3c37296d84728cb0418153ddd84a", []},
2221
"unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"},
2322
}

spec/brex/operator/aggregator_spec.exs

Lines changed: 0 additions & 5 deletions
This file was deleted.

0 commit comments

Comments
 (0)