SafeExpression evaluates a small expression language against values in an
Elixir map. Use it for configurable rules, filters, and routing decisions that
need comparisons and boolean logic but must not execute Elixir code.
The module uses a hand-written tokenizer, recursive-descent parser, and tree-walking evaluator. Expressions cannot call functions, spawn processes, or otherwise perform effects.
The grammar, from lowest to highest precedence, is:
expr := or
or := and ("||" and)*
and := eq ("&&" eq)*
eq := rel (("==" | "!=") rel)*
rel := unary (("<" | ">" | "<=" | ">=") unary)*
unary := "!" unary | primary
primary := number | string | "true" | "false" | "null" | "nil"
| path | "(" expr ")"
path := ident ("." ident)*Paths read string keys only. Bindings can contain any Elixir term. A bare path
returns that term unchanged. A missing segment, a non-map intermediate value,
and an explicitly stored nil all resolve to nil. The language does not
distinguish among these cases. nil is an alias for the null literal.
Equality uses Elixir's == and != semantics, including equality between
numerically equivalent integers and floats. Ordering accepts numbers only.
!, &&, and || accept booleans only. The evaluator does not coerce values.
Number literals are non-negative. A backslash in a string literal escapes the
next character verbatim; sequences such as \n do not produce a newline.
Expressions cannot execute arbitrary code. Evaluation does not limit CPU time
or memory use. Applications that accept untrusted expressions should limit the
length of source before they call eval/2.
Summary
Functions
Evaluates source against bindings. Paths read string keys only.
Types
Functions
Evaluates source against bindings. Paths read string keys only.
Returns {:ok, value} with the result's native type, or {:error, reason} for
malformed input or an operator type mismatch.
Examples
iex> SafeExpression.eval(~s(count >= 2 && kind == "webhook"), %{
...> "count" => 3,
...> "kind" => "webhook"
...> })
{:ok, true}
iex> SafeExpression.eval("payload", %{"payload" => {:queued, 42}})
{:ok, {:queued, 42}}
iex> SafeExpression.eval("value", %{value: 42})
{:ok, nil}