Packages
predicator
1.0.0
9.4.1
9.4.0
9.3.0
9.2.0
9.1.0
9.0.2
9.0.1
9.0.0
8.0.0
7.0.0
6.0.0
5.0.0
4.0.0
3.8.0
3.7.0
3.6.0
3.5.0
3.4.0
3.3.0
3.2.0
3.1.0
3.0.0
2.2.0
2.1.0
2.0.0
1.1.0
1.0.1
1.0.0
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.1
0.7.3
0.7.2
0.7.1
0.7.0
0.6.1
0.5.1
0.5.0
0.4.0
0.3.0
0.2.1
0.2.0
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
A secure, non-evaling condition (boolean predicate) engine for end users
Current section
Files
Jump to
Current section
Files
predicator
README.md
README.md
# Predicator
[](https://github.com/riddler/predicator-ex/actions/workflows/ci.yml)
[](https://codecov.io/gh/riddler/predicator-ex)
[](https://hex.pm/packages/predicator)
[](https://hexdocs.pm/predicator/)
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.
## Features
- 🔒 **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
## Quick Start
```elixir
# 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"
```
## Supported Operations
### Comparison Operators
| 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"` |
### Logical Operators
| 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` |
### Membership Operators
| Operator | Description | Example |
|----------|-------------|---------|
| `in` | Element in collection | `role in ["admin", "manager"]` |
| `contains` | Collection contains element | `[1, 2, 3] contains 2` |
### Built-in Functions
#### String Functions
| 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` |
#### Numeric Functions
| 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` |
#### Date Functions
| 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` |
## Data Types
- **Numbers**: `42`, `-17` (integers)
- **Strings**: `"hello"`, `"world"` (double-quoted with escape sequences)
- **Booleans**: `true`, `false` (or plain identifiers like `active`, `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)
## Architecture
Predicator uses a multi-stage compilation pipeline:
```
Expression String → Lexer → Parser → Compiler → Evaluator
↓ ↓ ↓ ↓ ↓
"score > 85 OR admin" → Tokens → AST → Instructions → Result
```
### Grammar
```ebnf
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 )* )? "]"
```
### Core Components
- **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
- **Functions**: Function system components
- **SystemFunctions** (`Predicator.Functions.SystemFunctions`): Built-in system functions
- **Registry** (`Predicator.Functions.Registry`): Function registration and dispatch
## Error Handling
Predicator provides detailed error information with exact positioning:
```elixir
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"}
```
## Advanced Usage
### Custom Function Registration
You can register your own custom functions for use in expressions:
```elixir
# 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)
```
### String Formatting Options
The StringVisitor supports multiple formatting modes:
```elixir
# 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)"
```
## Development
### Setup
```bash
mix deps.get
mix test
```
### Quality Checks
```bash
# Run all quality checks
mix quality
# Individual checks
mix format # Format code
mix credo --strict # Linting
mix coveralls # Test coverage
mix dialyzer # Type checking
```
### Test Coverage
Current 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)
```bash
mix test.coverage.html # Generate HTML coverage report
```
## Installation
Add `predicator` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:predicator, "~> 1.0.0"}
]
end
```
## Documentation
Full documentation is available at [HexDocs](https://hexdocs.pm/predicator).