Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 9 additions & 82 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,85 +7,12 @@ on:
- main

jobs:
mix_test:
name: mix test (Erlang/OTP ${{matrix.otp}} | Elixir ${{matrix.elixir}})
runs-on: ubuntu-latest
container: hexpm/elixir:${{ matrix.elixir }}-erlang-${{ matrix.otp }}-alpine-3.11.6
strategy:
fail-fast: false
matrix:
include:
- elixir: 1.6.6
otp: 19.3.6.13
- elixir: 1.7.4
otp: 19.3.6.13
- elixir: 1.8.2
otp: 20.3.8.26
- elixir: 1.9.4
otp: 20.3.8.26
- elixir: 1.10.3
otp: 21.3.8.16
env:
MIX_ENV: test
steps:
- uses: actions/checkout@v2.3.1

- name: Cache - deps/
uses: actions/cache@v1
with:
path: deps/
key: alpine-elixir-${{ matrix.elixir }}-otp-${{ matrix.otp }}-deps-${{ hashFiles('**/mix.lock') }}
restore-keys: alpine-elixir-${{ matrix.elixir }}-otp-${{ matrix.otp }}-deps-

- name: Install Dependencies
run: |
mix local.rebar --force
mix local.hex --force
mix deps.get

- name: Cache - _build/
uses: actions/cache@v1
with:
path: _build/
key: alpine-elixir-${{ matrix.elixir }}-otp-${{ matrix.otp }}-build-${{ hashFiles('**/mix.lock') }}
restore-keys: alpine-elixir-${{ matrix.elixir }}-otp-${{ matrix.otp }}-build-

- run: mix compile
- name: Run tests
run: |
# coveralls.github requires git to be available
apk add --no-cache git > /dev/null

mix coveralls.github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

check_style:
name: Check style
runs-on: ubuntu-latest
container: hexpm/elixir:1.10.3-erlang-22.1.8.1-alpine-3.11.6
steps:
- uses: actions/checkout@v2.3.1

- name: Cache - deps/
uses: actions/cache@v1
with:
path: deps/
key: alpine-deps-${{ hashFiles('**/mix.lock') }}
restore-keys: alpine-deps-

- name: Install Dependencies
run: |
mix local.rebar --force
mix local.hex --force
mix deps.get

- name: Cache - _build/
uses: actions/cache@v1
with:
path: _build/
key: alpine-build-${{ hashFiles('**/mix.lock') }}
restore-keys: alpine-build-

- run: mix format --check-formatted
- run: mix credo
ci:
uses: systemic-engineer/ci/.github/workflows/elixir-matrix.yml@main
with:
matrix: |
[
{"elixir": "1.18", "otp": "27.0", "alpine": "3.20"},
{"elixir": "1.17", "otp": "27.0", "alpine": "3.20"},
{"elixir": "1.16", "otp": "26.2", "alpine": "3.20"}
]
20 changes: 7 additions & 13 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,11 @@ on:
types: [published]

jobs:
publish_to_hex:
publish:
name: Publish to Hex.pm
runs-on: ubuntu-latest
container: hexpm/elixir:1.10.3-erlang-22.1.8.1-alpine-3.11.6
steps:
- uses: actions/checkout@v2.3.1
- name: Install dependencies
run: |
mix local.rebar --force
mix local.hex --force
mix deps.get
- run: mix hex.publish --yes
env:
HEX_API_KEY: ${{ secrets.HEX_API_KEY }}
uses: systemic-engineer/ci/.github/workflows/hex-publish.yml@main
with:
elixir-version: "1.18"
otp-version: "27.0"
secrets:
hex-api-key: ${{ secrets.HEX_API_KEY }}
21 changes: 21 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# List all commands
default:
@just --list

# Run all tests
test:
mix test

# Run only tests affected by recent changes (faster)
test-stale:
mix test --stale

# Format code
format:
mix format

# Pre-commit gate: run by the global TDD commit-msg hook
pre-commit: test-stale

# Pre-push gate: full test suite before pushing
pre-push: test
2 changes: 1 addition & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config
import Config

# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
Expand Down
34 changes: 34 additions & 0 deletions lib/brex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,40 @@ defmodule Brex do
@spec number_of_clauses(Types.rule()) :: non_neg_integer()
defdelegate number_of_clauses(rule), to: Rule

@doc """
Evaluates a rule and returns a `Tracer.t()` tree for visual inspection.

Equivalent to `evaluate/2` followed by `Brex.Trace.from_result/1`. Allows you
to pass a list of rules which get linked calling `all/1`.

The resulting `Tracer` can be rendered as a color-coded tree via `inspect/2`.
See `Brex.Trace` and `Tracer` for details on inspect options.

# Examples

iex> trace = Brex.trace(&is_list/1, [1, 2])
iex> Tracer.ok?(trace)
true

iex> trace = Brex.trace(Brex.all([&is_list/1, &Keyword.keyword?/1]), [a: 1])
iex> output = inspect(trace, custom_options: [depth: :infinity], syntax_colors: [])
iex> output =~ "Tracer<OK>"
true
iex> output =~ ":all"
true

iex> trace = Brex.trace(Brex.any([&is_list/1, &is_map/1]), "hello")
iex> output = inspect(trace, custom_options: [depth: :error], syntax_colors: [])
iex> output =~ "Tracer<ERROR>"
true
"""
@spec trace(one_or_many_rules(), value()) :: Tracer.t()
def trace(rules, value) do
rules
|> evaluate(value)
|> Brex.Trace.from_result()
end

operator_doc = fn operator ->
"""
Links the given rules in a boolean fashion, similar to the `Enum` functions.
Expand Down
2 changes: 0 additions & 2 deletions lib/brex/result/formatter/rules.ex
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ defmodule Brex.Result.Formatter.Rules do

iex> Brex.Result.Formatter.Rules.format([:foo])
** (ArgumentError) Invalid result! Expected a list of or single `Brex.Result` struct but received: [:foo]
(brex) lib/brex/result/formatter.ex:45: Brex.Result.Formatter.invalid_result!/1
(elixir) lib/enum.ex:1294: Enum."-map/2-lists^map/1-0-"/2
"""

use Brex.Result.Formatter
Expand Down
101 changes: 101 additions & 0 deletions lib/brex/trace.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
defmodule Brex.Trace do
@moduledoc """
Converts a `Brex.Result` evaluation tree into a `Tracer.t()` for visual
inspection.

Each result becomes a node in the trace tree:

- `step` — the rule identity:
* operators → `:all`, `:any`, `:none` (or the aggregator's function name)
* named functions → `{module, name, arity}` MFA tuple
* anonymous functions → `:anonymous_fn`
* struct-based rules → the struct module
* module-based rules → the module atom
- `input` — the evaluated value
- `output` — `{:ok, :passed}` / `{:error, :failed}` for operators; raw
evaluation (`true`, `false`, `:ok`, `{:error, reason}`, etc.) for leaf rules
- `nested` — child traces for operator rules; `[]` for leaf rules

Use `inspect/2` on the returned `Tracer.t()` to render a color-coded tree.
See `Tracer` inspect options (`depth`, `indent`) for controlling output depth.

## Examples

iex> result = Brex.evaluate(&is_list/1, [1, 2])
iex> trace = Brex.Trace.from_result(result)
iex> trace.step
{:erlang, :is_list, 1}
iex> trace.input
[1, 2]
iex> trace.output
true

iex> result = Brex.evaluate(&is_list/1, "not a list")
iex> Brex.Trace.from_result(result) |> Tracer.error?()
true

iex> result = Brex.evaluate(Brex.all([&is_list/1, &Keyword.keyword?/1]), [a: 1])
iex> trace = Brex.Trace.from_result(result)
iex> trace.step
:all
iex> trace.output
{:ok, :passed}
iex> length(trace.nested)
2

iex> result = Brex.evaluate(Brex.any([&is_list/1, &is_map/1]), "hello")
iex> trace = Brex.Trace.from_result(result)
iex> trace.output
{:error, :failed}
iex> length(trace.nested)
2

iex> result = Brex.evaluate(Brex.none([&is_list/1, &is_map/1]), "hello")
iex> trace = Brex.Trace.from_result(result)
iex> trace.step
:none
iex> Tracer.ok?(trace)
true

iex> double = fn x -> x * 2 end
iex> result = Brex.evaluate(double, 3)
iex> Brex.Trace.from_result(result).step
:anonymous_fn
"""

@spec from_result(Brex.Result.t()) :: Tracer.t()
def from_result(%Brex.Result{rule: %Brex.Operator{} = rule, value: value, evaluation: evaluation}) do
{output, nested} =
case evaluation do
{:ok, results} -> {{:ok, :passed}, Enum.map(results, &from_result/1)}
{:error, results} -> {{:error, :failed}, Enum.map(results, &from_result/1)}
end

Tracer.new(step(rule), value, output, nested)
end

def from_result(%Brex.Result{rule: rule, value: value, evaluation: evaluation}) do
Tracer.new(step(rule), value, evaluation, [])
end

defp step(%Brex.Operator{aggregator: agg}) do
case Function.info(agg, :name) do
{:name, :all?} -> :all
{:name, :any?} -> :any
{:name, :none?} -> :none
{:name, name} -> name
end
end

defp step(fun) when is_function(fun) do
info = Function.info(fun)

case info[:type] do
:external -> {info[:module], info[:name], info[:arity]}
:local -> :anonymous_fn
end
end

defp step(struct) when is_struct(struct), do: struct.__struct__
defp step(other), do: other
end
12 changes: 6 additions & 6 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ defmodule Brex.Mixfile do
elixir: "~> 1.5",
elixirc_paths: elixirc_paths(Mix.env()),
preferred_cli_env: [
espec: :test,
coveralls: :test,
"coveralls.detail": :test,
"coveralls.post": :test,
Expand Down Expand Up @@ -40,26 +39,27 @@ defmodule Brex.Mixfile do
end

# Specifies which paths to compile per environment.
defp elixirc_paths(:test), do: ["lib", "spec/support"]
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]

# Run "mix help deps" to learn about dependencies.
defp deps do
[
# Runtime
{:tracer, github: "systemic-engineer/tracer"},

# No Runtime
{:ex_doc, "~> 0.19", only: :dev, runtime: false},

# Test
{:credo, "~> 1.4", only: :dev, runtime: false},
{:excoveralls, "~> 0.13", only: :test},
{:espec, "~> 1.6", only: :test}
{:ssl_verify_fun, "~> 1.1.7", only: :test, override: true}
]
end

def aliases do
[
test: "espec"
]
[]
end

def description do
Expand Down
5 changes: 2 additions & 3 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"},
"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"},
"earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm", "b42a23e9bd92d65d16db2f75553982e58519054095356a418bb8320bbacb58b1"},
"espec": {:hex, :espec, "1.6.3", "d9355788e508b82743a1b1b9aa5ac64ba37b0547c6210328d909e8a6eb56d42e", [:mix], [{:meck, "0.8.12", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "235ef9931fc6ae8066272b77dc11c462e72af0aa50c6023643acd22b09326d21"},
"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"},
"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"},
"exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"},
Expand All @@ -13,11 +12,11 @@
"jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm"},
"makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d7152ff93f2eac07905f510dfa03397134345ba4673a00fbf7119bab98632940"},
"makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "4a36dd2d0d5c5f98d95b3f410d7071cd661d5af310472229dd0e92161f168a44"},
"meck": {:hex, :meck, "0.8.12", "1f7b1a9f5d12c511848fec26bbefd09a21e1432eadb8982d9a8aceb9891a3cf2", [:rebar3], [], "hexpm", "7a6ab35a42e6c846636e8ecd6fdf2cc2e3f09dbee1abb15c1a7c705c10775787"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm", "ebb595e19456a72786db6dcd370d320350cb624f0b6203fcc7e23161d49b0ffb"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"tracer": {:git, "https://github.com/systemic-engineer/tracer.git", "8f3bcc3a051f3c37296d84728cb0418153ddd84a", []},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"},
}
5 changes: 0 additions & 5 deletions spec/brex/operator/aggregator_spec.exs

This file was deleted.

5 changes: 0 additions & 5 deletions spec/brex/operator_spec.exs

This file was deleted.

5 changes: 0 additions & 5 deletions spec/brex/result/formatter/rules_spec.exs

This file was deleted.

Loading