SafeExpression (SafeExpression v0.1.0)

Copy Markdown View Source

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

ordering_operator()

@type ordering_operator() :: :lt | :gt | :lte | :gte

reason()

@type reason() ::
  :invalid_utf8
  | :unterminated_string
  | :unexpected_end
  | :missing_closing_parenthesis
  | :trailing_dot
  | :unexpected_token
  | {:unexpected_character, binary()}
  | {:invalid_number, binary()}
  | {:not_boolean, term()}
  | {:not_comparable, ordering_operator(), term(), term()}

value()

@type value() :: term()

Functions

eval(source, bindings)

@spec eval(String.t(), map()) :: {:ok, value()} | {:error, reason()}

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}