philiprehberger-safe_exec
Sandboxed expression evaluator with whitelisted operations
Requirements
- Ruby >= 3.1
Installation
Add to your Gemfile:
gem "philiprehberger-safe_exec"
Or install directly:
gem install philiprehberger-safe_exec
Usage
require "philiprehberger/safe_exec"
Philiprehberger::SafeExec.evaluate('2 + 3 * 4') # => 14
Philiprehberger::SafeExec.evaluate('(2 + 3) * 4') # => 20
Philiprehberger::SafeExec.evaluate('price * 1.08', { price: 100 }) # => 108.0
Arithmetic
Philiprehberger::SafeExec.evaluate('10 + 5') # => 15
Philiprehberger::SafeExec.evaluate('10 - 5') # => 5
Philiprehberger::SafeExec.evaluate('10 * 5') # => 50
Philiprehberger::SafeExec.evaluate('10 / 3') # => 3 (integer division)
Philiprehberger::SafeExec.evaluate('10.0 / 3') # => 3.333...
Philiprehberger::SafeExec.evaluate('10 % 3') # => 1
Philiprehberger::SafeExec.evaluate('2 ** 10') # => 1024
Comparisons and Booleans
Philiprehberger::SafeExec.evaluate('5 > 3') # => true
Philiprehberger::SafeExec.evaluate('5 == 5') # => true
Philiprehberger::SafeExec.evaluate('true && false') # => false
Philiprehberger::SafeExec.evaluate('!false || true') # => true
Philiprehberger::SafeExec.evaluate('age >= 18 && age < 65', { age: 25 }) # => true
String Operations
Philiprehberger::SafeExec.evaluate("'hello' + ' ' + 'world'") # => "hello world"
Philiprehberger::SafeExec.evaluate("name == 'Alice'", { name: 'Alice' }) # => true
Ternary Operator
Philiprehberger::SafeExec.evaluate('true ? 1 : 2') # => 1
Philiprehberger::SafeExec.evaluate('5 > 3 ? 10 : 20') # => 10
Philiprehberger::SafeExec.evaluate("age >= 18 ? 'adult' : 'minor'", { age: 25 }) # => "adult"
Built-in Functions
Philiprehberger::SafeExec.evaluate('min(3, 7)') # => 3
Philiprehberger::SafeExec.evaluate('max(3, 7)') # => 7
Philiprehberger::SafeExec.evaluate('abs(-5)') # => 5
Philiprehberger::SafeExec.evaluate("length('hello')") # => 5
Philiprehberger::SafeExec.evaluate('round(3.14159, 2)') # => 3.14
Philiprehberger::SafeExec.evaluate('sqrt(16)') # => 4.0
Philiprehberger::SafeExec.evaluate('ceil(3.2)') # => 4
Philiprehberger::SafeExec.evaluate('floor(3.9)') # => 3
Philiprehberger::SafeExec.evaluate('pow(2, 10)') # => 1024
String Functions
Philiprehberger::SafeExec.evaluate("upcase('hello')") # => "HELLO"
Philiprehberger::SafeExec.evaluate("downcase('HELLO')") # => "hello"
Philiprehberger::SafeExec.evaluate("trim(' hello ')") # => "hello"
Hash and Array Access
context = { items: [10, 20, 30], user: { 'name' => 'Alice', 'role' => 'admin' } }
Philiprehberger::SafeExec.evaluate('items[0]', context) # => 10
Philiprehberger::SafeExec.evaluate("user['name']", context) # => "Alice"
Philiprehberger::SafeExec.evaluate('user.role', context) # => "admin"
Compiled Expressions
Pre-parse an expression once and evaluate it many times against different contexts — useful for rules engines and calculation pipelines.
formula = Philiprehberger::SafeExec.compile('price * (1 + tax_rate) - discount')
formula.evaluate(price: 100, tax_rate: 0.08, discount: 5) # => 103.0
formula.evaluate(price: 50, tax_rate: 0.10, discount: 0) # => 55.0
formula.source # => "price * (1 + tax_rate) - discount"
Syntax errors raise immediately at compile time, before any evaluation.
Timeout
Philiprehberger::SafeExec.evaluate('1 + 1', {}, timeout: 2)
# Raises Philiprehberger::SafeExec::TimeoutError if evaluation exceeds 2 seconds
API
| Method | Description |
|---|---|
SafeExec.evaluate(expr, context = {}, timeout: 5) |
Evaluate a sandboxed expression string with optional context variables and timeout (seconds) |
SafeExec.compile(expr) |
Pre-parse an expression and return a reusable Compiled instance |
Compiled#evaluate(context = {}, timeout: 5) |
Evaluate the compiled expression against a context |
Compiled#source |
The original expression source string |
SafeExec::Error |
Base error class for parse and evaluation failures |
SafeExec::TimeoutError |
Raised when evaluation exceeds the timeout |
SafeExec::DEFAULT_TIMEOUT |
Default timeout in seconds (5) |
SafeExec::VERSION |
Gem version string |
Tokenizer.tokenize(input) |
Tokenize an expression string into an array of Token structs |
Tokenizer::Token |
Struct with type (Symbol) and value (String) fields |
Tokenizer::TOKEN_PATTERNS |
Ordered array of [type, regex] pairs used by the tokenizer |
Parser.new(tokens) |
Create a parser from an array of Token structs |
Parser#parse |
Parse the token stream into an AST hash |
Evaluator.new(context = {}) |
Create an evaluator with a context hash (keys are normalized to strings) |
Evaluator#evaluate(node) |
Evaluate an AST node and return the result |
Supported Operations
| Category | Operations |
|---|---|
| Arithmetic | +, -, *, /, %, ** |
| Comparison | ==, !=, >, <, >=, <= |
| Boolean | &&, `\ |
| Ternary | condition ? value_if_true : value_if_false |
| String | concatenation via +, comparison |
| Functions | min(a, b), max(a, b), abs(n), length(str_or_arr), round(n, precision), sqrt(n), ceil(n), floor(n), pow(base, exp), upcase(str), downcase(str), trim(str) |
| Access | array[index], hash['key'], hash.key |
| Literals | integers, floats, strings, booleans, nil |
| Grouping | parentheses () |
Development
bundle install
bundle exec rspec
bundle exec rubocop
Support
If you find this project useful: