Compiled condition AST and helpers.
A condition is a small data tree, not a closure. This makes rules introspectable, testable, and serialisable.
All variants use only 2-tuples (and lists) so they survive Absinthe's schema metadata pipeline intact.
Grammar
condition ::=
| :always # vacuously true
| {:literal, term()}
| {:arg, atom()} # GraphQL argument
| {:loaded, [atom()]} # [load_name, field, ...]
| {:current_user, [atom()]} # path under context.current_user
| {:context, [atom()]} # arbitrary context path
| {:cmp, [op() | expr()]} # [op, lhs, rhs]
| {:and, [condition()]}
| {:or, [condition()]}
| {:not, condition()}
| {:fun, mfa_or_capture()} # escape hatchComparison operators: :eq, :neq, :gt, :gte, :lt, :lte,
:in, :not_in.
Why 2-tuples only
Absinthe stores schema metadata through an AST-aware pipeline that
treats any 3-element tuple as a macro call. Sticking to 2-tuples
means rules survive the round-trip and remain plain data — readable
for tests, logs, the mix absinthe_permission.audit task, and AI
agents inspecting the schema.
Summary
Functions
Pretty-print a condition as a one-line string. Used by audit output and error messages.
Returns true if op is a supported comparison operator.
Returns the list of supported comparison operators.
Types
@type op() :: :eq | :neq | :gt | :gte | :lt | :lte | :in | :not_in
Functions
Pretty-print a condition as a one-line string. Used by audit output and error messages.
iex> AbsinthePermission.Condition.format(:always)
"always"
iex> AbsinthePermission.Condition.format({:cmp, [:eq, {:arg, :state}, {:literal, "CLOSED"}]})
"arg(:state) == \"CLOSED\""
Returns true if op is a supported comparison operator.
@spec valid_ops() :: [op()]
Returns the list of supported comparison operators.