A secure, non-evaluative condition engine for processing end-user boolean predicates in Elixir. Predicator allows you to safely evaluate user-defined expressions without the security risks of dynamic code execution.
- 🔒 Secure: No
eval()or dynamic code execution - safe for end-user input - 🎯 Simple: Clean, intuitive expression syntax (
score > 85,name = "John") - 🚀 Fast: Compiled expressions execute efficiently with minimal overhead
- 🛡️ Type Safe: Built with comprehensive specs and rigorous testing
- 🎨 Flexible: Support for literals, identifiers, comparisons, and parentheses
- 📊 Observable: Detailed error reporting with line/column information
- 🔄 Reversible: Convert AST back to string expressions with formatting options
- 📅 Date Support: Native date and datetime literals with ISO 8601 format
- 📋 Lists: List literals with membership operations (
in,contains) - 🧠 Smart Logic: Logical operators with proper precedence (
AND,OR,NOT) - 🔧 Functions: Built-in functions for string, numeric, and date operations
# Basic evaluation
iex> Predicator.evaluate!("score > 85", %{"score" => 92})
true
# String comparisons
iex> Predicator.evaluate!("name = \"Alice\"", %{"name" => "Alice"})
true
# Date and datetime literals
iex> Predicator.evaluate!("#2024-01-15# > #2024-01-10#", %{})
true
iex> Predicator.evaluate!("created_at < #2024-01-15T10:30:00Z#", %{"created_at" => ~U[2024-01-10 09:00:00Z]})
true
# List literals and membership
iex> Predicator.evaluate!("role in [\"admin\", \"manager\"]", %{"role" => "admin"})
true
iex> Predicator.evaluate!("[1, 2, 3] contains 2", %{})
true
# Logical operators with proper precedence
iex> Predicator.evaluate!("score > 85 AND age >= 18", %{"score" => 92, "age" => 25})
true
iex> Predicator.evaluate!("role = \"admin\" OR role = \"manager\"", %{"role" => "admin"})
true
iex> Predicator.evaluate!("NOT expired AND active", %{"expired" => false, "active" => true})
true
# Complex expressions with parentheses
iex> Predicator.evaluate!("(score > 85 OR admin) AND active", %{"score" => 80, "admin" => true, "active" => true})
true
# Built-in functions
iex> Predicator.evaluate!("len(name) > 3", %{"name" => "Alice"})
true
iex> Predicator.evaluate!("upper(role) = \"ADMIN\"", %{"role" => "admin"})
true
iex> Predicator.evaluate!("year(created_at) = 2024", %{"created_at" => ~D[2024-03-15]})
true
# Compile once, evaluate many times for performance
iex> {:ok, instructions} = Predicator.compile("score > threshold AND active")
iex> Predicator.evaluate!(instructions, %{"score" => 95, "threshold" => 80, "active" => true})
true
# Using evaluate/2 (returns {:ok, result} or {:error, message})
iex> Predicator.evaluate("score > 85", %{"score" => 92})
{:ok, true}
iex> Predicator.evaluate("invalid >> syntax", %{})
{:error, "Expected number, string, boolean, date, datetime, identifier, function call, list, or '(' but found '>' at line 1, column 10"}
# Using evaluate/1 for expressions without context (strings or instruction lists)
iex> Predicator.evaluate("#2024-01-15# > #2024-01-10#")
{:ok, true}
iex> Predicator.evaluate([["lit", 42]])
{:ok, 42}
# Round-trip: parse and decompile expressions
iex> {:ok, ast} = Predicator.parse("score > 85 AND #2024-01-15# in dates")
iex> Predicator.decompile(ast)
"score > 85 AND #2024-01-15# IN dates"| Operator | Description | Example |
|---|---|---|
> |
Greater than | score > 85, #2024-01-15# > #2024-01-10# |
< |
Less than | age < 30, created_at < #2024-01-15T10:00:00Z# |
>= |
Greater than or equal | points >= 100 |
<= |
Less than or equal | count <= 5 |
= |
Equal | status = "active", date = #2024-01-15# |
!= |
Not equal | role != "guest" |
| Operator | Description | Example |
|---|---|---|
AND |
Logical AND (case-insensitive) | score > 85 AND age >= 18 |
OR |
Logical OR (case-insensitive) | role = "admin" OR role = "manager" |
NOT |
Logical NOT (case-insensitive) | NOT expired |
| Operator | Description | Example |
|---|---|---|
in |
Element in collection | role in ["admin", "manager"] |
contains |
Collection contains element | [1, 2, 3] contains 2 |
| Function | Description | Example |
|---|---|---|
len(string) |
String length | len(name) > 3 |
upper(string) |
Convert to uppercase | upper(role) = "ADMIN" |
lower(string) |
Convert to lowercase | lower(name) = "alice" |
trim(string) |
Remove whitespace | len(trim(input)) > 0 |
| Function | Description | Example |
|---|---|---|
abs(number) |
Absolute value | abs(balance) < 100 |
max(a, b) |
Maximum of two numbers | max(score1, score2) > 85 |
min(a, b) |
Minimum of two numbers | min(age, 65) >= 18 |
| Function | Description | Example |
|---|---|---|
year(date) |
Extract year | year(created_at) = 2024 |
month(date) |
Extract month | month(birthday) = 12 |
day(date) |
Extract day | day(deadline) <= 15 |
- Numbers:
42,-17(integers) - Strings:
"hello","world"(double-quoted with escape sequences) - Booleans:
true,false(or plain identifiers likeactive,expired) - Dates:
#2024-01-15#(ISO 8601 date format) - DateTimes:
#2024-01-15T10:30:00Z#(ISO 8601 datetime format with timezone) - Lists:
[1, 2, 3],["admin", "manager"](homogeneous collections) - Identifiers:
score,user_name,is_active(variable references)
Predicator uses a multi-stage compilation pipeline:
Expression String → Lexer → Parser → Compiler → Instructions → Evaluator
↓ ↓ ↓ ↓ ↓ ↓
"score > 85 AND #2024-01-15# in dates" → Tokens → AST → Instructions → Result
expression → logical_or
logical_or → logical_and ( ("OR" | "or") logical_and )*
logical_and → logical_not ( ("AND" | "and") logical_not )*
logical_not → ("NOT" | "not") logical_not | comparison
comparison → primary ( ( ">" | "<" | ">=" | "<=" | "=" | "!=" | "in" | "contains" ) primary )?
primary → NUMBER | STRING | BOOLEAN | DATE | DATETIME | IDENTIFIER | function_call | list | "(" expression ")"
function_call → FUNCTION_NAME "(" ( expression ( "," expression )* )? ")"
list → "[" ( expression ( "," expression )* )? "]"- Lexer (
Predicator.Lexer): Tokenizes input with position tracking - Parser (
Predicator.Parser): Builds Abstract Syntax Tree with error reporting - Compiler (
Predicator.Compiler): Converts AST to executable instructions - Evaluator (
Predicator.Evaluator): Executes instructions against data - Visitors: AST transformation modules
- StringVisitor (
Predicator.Visitors.StringVisitor): Converts AST back to expressions - InstructionsVisitor (
Predicator.Visitors.InstructionsVisitor): Converts AST to instructions
- StringVisitor (
- Functions: Function system components
- SystemFunctions (
Predicator.Functions.SystemFunctions): Built-in system functions - Registry (
Predicator.Functions.Registry): Function registration and dispatch
- SystemFunctions (
Predicator provides detailed error information with exact positioning:
iex> Predicator.evaluate("score >> 85", %{})
{:error, "Unexpected character '>' at line 1, column 8"}
iex> Predicator.evaluate("score AND", %{})
{:error, "Expected number, string, boolean, date, datetime, identifier, function call, list, or '(' but found end of input at line 1, column 1"}You can register your own custom functions for use in expressions:
# Register a simple function
Predicator.register_function("double", 1, fn [n], _context ->
{:ok, n * 2}
end)
# Use in expressions
iex> Predicator.evaluate("double(score) > 100", %{"score" => 60})
{:ok, true}
# Context-aware function
Predicator.register_function("user_role", 0, fn [], context ->
{:ok, Map.get(context, "current_user_role", "guest")}
end)
iex> Predicator.evaluate("user_role() = \"admin\"", %{"current_user_role" => "admin"})
{:ok, true}
# Function with error handling
Predicator.register_function("divide", 2, fn [a, b], _context ->
if b == 0 do
{:error, "Division by zero"}
else
{:ok, a / b}
end
end)The StringVisitor supports multiple formatting modes:
# Compact formatting (no spaces)
iex> Predicator.decompile(ast, spacing: :compact)
"score>85"
# Verbose formatting (extra spaces)
iex> Predicator.decompile(ast, spacing: :verbose)
"score > 85"
# Explicit parentheses
iex> Predicator.decompile(ast, parentheses: :explicit)
"(score > 85)"For repeated evaluations with the same expression:
# Compile once
{:ok, instructions} = Predicator.compile("score > threshold")
# Evaluate many times
results =
data_list
|> Enum.map(&Predicator.evaluate(instructions, &1))
|> Enum.map(fn {:ok, result} -> result end)mix deps.get
mix test# Run all quality checks
mix quality
# Individual checks
mix format # Format code
mix credo --strict # Linting
mix coveralls # Test coverage
mix dialyzer # Type checkingCurrent coverage: 92.6% overall with comprehensive testing across all components:
- Lexer: 100% (including date/datetime tokenization)
- Types: 100% (including date type checking)
- Evaluator: 90.1% (all operations and error conditions)
- Parser: 86.8% (complex expressions and edge cases)
- StringVisitor: 94.8% (formatting and decompilation)
mix test.coverage.html # Generate HTML coverage reportAdd predicator to your list of dependencies in mix.exs:
def deps do
[
{:predicator, "~> 1.0.0"}
]
endFull documentation is available at HexDocs.