ExJexl

Disclaimer

This code has been pretty much one shotted by Claude. I haven't tested it in a production setting. It might be full of security issues. USE AT YOUR OWN RISK!

Hex.pmDocumentationLicense

A powerful and fast JEXL (JavaScript Expression Language) evaluator for Elixir, built with NimbleParsec for excellent performance and comprehensive feature support.

Features

โœจ Comprehensive Language Support

โšก High Performance

๐Ÿ”ง Developer Friendly

Installation

Add ex_jexl to your list of dependencies in mix.exs:

def deps do
  [
    {:ex_jexl, "~> 0.1.0"}
  ]
end

Quick Start

# Simple arithmetic
ExJexl.eval("2 + 3 * 4")
# => {:ok, 14}

# Working with context
context = %{
  "user" => %{"name" => "Alice", "age" => 30},
  "items" => [1, 2, 3, 4, 5]
}

ExJexl.eval("user.name", context)
# => {:ok, "Alice"}

ExJexl.eval("user.age >= 18", context)
# => {:ok, true}

ExJexl.eval("items|length", context)
# => {:ok, 5}

# Complex expressions
ExJexl.eval("user.age >= 18 && items|length > 0", context)
# => {:ok, true}

Language Reference

Literals

ExJexl.eval("42")           # => {:ok, 42}
ExJexl.eval("3.14")         # => {:ok, 3.14}  
ExJexl.eval("\"hello\"")    # => {:ok, "hello"}
ExJexl.eval("true")         # => {:ok, true}
ExJexl.eval("null")         # => {:ok, nil}
ExJexl.eval("[1, 2, 3]")    # => {:ok, [1, 2, 3]}
ExJexl.eval("{\"key\": \"value\"}")  # => {:ok, %{"key" => "value"}}

Arithmetic Operations

ExJexl.eval("10 + 5")       # => {:ok, 15}
ExJexl.eval("10 - 3")       # => {:ok, 7}
ExJexl.eval("4 * 5")        # => {:ok, 20}
ExJexl.eval("15 / 3")       # => {:ok, 5.0}
ExJexl.eval("17 % 5")       # => {:ok, 2}
ExJexl.eval("2 + 3 * 4")    # => {:ok, 14} (respects precedence)
ExJexl.eval("(2 + 3) * 4")  # => {:ok, 20}

Comparison Operations

ExJexl.eval("5 == 5")       # => {:ok, true}
ExJexl.eval("5 != 3")       # => {:ok, true}
ExJexl.eval("10 > 5")       # => {:ok, true}
ExJexl.eval("3 <= 5")       # => {:ok, true}
ExJexl.eval("\"apple\" < \"banana\"")  # => {:ok, true}

Logical Operations

ExJexl.eval("true && false")   # => {:ok, false}
ExJexl.eval("true || false")   # => {:ok, true}
ExJexl.eval("!true")           # => {:ok, false}

# Short-circuit evaluation
ExJexl.eval("false && expensive_call", context)  # expensive_call not evaluated

Property Access

context = %{
  "user" => %{"name" => "Alice", "profile" => %{"email" => "alice@example.com"}},
  "items" => [10, 20, 30],
  "key" => "name"
}

# Dot notation
ExJexl.eval("user.name", context)                    # => {:ok, "Alice"}
ExJexl.eval("user.profile.email", context)          # => {:ok, "alice@example.com"}

# Bracket notation  
ExJexl.eval("user[\"name\"]", context)              # => {:ok, "Alice"}
ExJexl.eval("user[key]", context)                   # => {:ok, "Alice"}

# Array access
ExJexl.eval("items[0]", context)                    # => {:ok, 10}
ExJexl.eval("items[1]", context)                    # => {:ok, 20}

# Mixed access
ExJexl.eval("user.profile[\"email\"]", context)    # => {:ok, "alice@example.com"}

Membership Testing

context = %{
  "numbers" => [1, 2, 3, 4, 5],
  "user" => %{"name" => "Alice", "age" => 30},
  "text" => "hello world"
}

# Array membership
ExJexl.eval("3 in numbers", context)        # => {:ok, true}
ExJexl.eval("10 in numbers", context)       # => {:ok, false}

# Object key membership  
ExJexl.eval("\"name\" in user", context)    # => {:ok, true}
ExJexl.eval("\"email\" in user", context)   # => {:ok, false}

# String substring membership
ExJexl.eval("\"world\" in text", context)   # => {:ok, true}
ExJexl.eval("\"foo\" in text", context)     # => {:ok, false}

Transforms

Transforms allow you to manipulate and process data using a pipe syntax:

context = %{
  "numbers" => [3, 1, 4, 1, 5],
  "text" => "Hello World",
  "items" => ["apple", "banana", "cherry"]
}

# Array transforms
ExJexl.eval("numbers|length", context)      # => {:ok, 5}
ExJexl.eval("numbers|first", context)       # => {:ok, 3}
ExJexl.eval("numbers|last", context)        # => {:ok, 5}
ExJexl.eval("numbers|reverse", context)     # => {:ok, [5, 1, 4, 1, 3]}
ExJexl.eval("numbers|sort", context)        # => {:ok, [1, 1, 3, 4, 5]}
ExJexl.eval("numbers|unique", context)      # => {:ok, [3, 1, 4, 5]}

# String transforms  
ExJexl.eval("text|upper", context)          # => {:ok, "HELLO WORLD"}
ExJexl.eval("text|lower", context)          # => {:ok, "hello world"}

# Chained transforms
ExJexl.eval("numbers|reverse|first", context)        # => {:ok, 5}
ExJexl.eval("items|sort|reverse|first", context)     # => {:ok, "cherry"}

Available Transforms

Array Transforms:

String Transforms:

Object Transforms:

Utility Transforms:

Built-in Functions

context = %{
  "items" => [1, 2, 3],
  "user" => %{"name" => "Alice", "age" => 30}
}

ExJexl.eval("length(items)", context)       # => {:ok, 3}
ExJexl.eval("keys(user)", context)          # => {:ok, ["name", "age"]} 
ExJexl.eval("values(user)", context)        # => {:ok, ["Alice", 30]}
ExJexl.eval("type(items)", context)         # => {:ok, "array"}

Error Handling

ExJexl provides detailed error information for debugging:

# Syntax errors
ExJexl.eval("1 + + 2")
# => {:error, "expected end of string"}

# Runtime errors
ExJexl.eval("10 / 0") 
# => {:error, "Division by zero"}

# Use eval! for exceptions
ExJexl.eval!("10 / 0")
# => ** (RuntimeError) JEXL evaluation error: "Division by zero"

Context and Data Types

Context Keys

ExJexl supports both string and atom keys in contexts:

# String keys
string_context = %{"name" => "Alice", "age" => 30}
ExJexl.eval("name", string_context)  # => {:ok, "Alice"}

# Atom keys  
atom_context = %{name: "Bob", age: 25}
ExJexl.eval("name", atom_context)    # => {:ok, "Bob"}

# Mixed keys work too
mixed_context = %{"name" => "Charlie", :age => 35}
ExJexl.eval("name", mixed_context)   # => {:ok, "Charlie"}

Data Type Support

JEXL Type Elixir Type Example
numberinteger, float42, 3.14
stringbinary"hello"
booleanbooleantrue, false
nullnilnull
arraylist[1, 2, 3]
objectmap%{"key" => "value"}

Performance

ExJexl is designed for high performance:

Operation Type Throughput Use Case
Simple literals ~960K ops/sec Configuration values
Property access ~780K ops/sec Data extraction
Arithmetic ~460K ops/sec Calculations
Complex expressions ~122K ops/sec Business rules

Performance Tips

๐Ÿš€ Optimize for Speed:

# For repeated evaluation, cache the AST
{:ok, ast} = ExJexl.Parser.parse("user.age >= 18 && active")

# Then evaluate multiple times with different contexts
ExJexl.Evaluator.eval(ast, context1)
ExJexl.Evaluator.eval(ast, context2)

Use Cases

ExJexl is perfect for:

๐ŸŽฏ Business Rules Engine

rule = "user.age >= 18 && user.country in [\"US\", \"CA\"] && account.balance > 100"
ExJexl.eval(rule, user_context)

๐Ÿ“‹ Dynamic Configuration

config = "environment == \"production\" && feature_flags.new_ui_enabled"
ExJexl.eval(config, app_context)

๐Ÿ” Data Filtering

filter = "created_date >= \"2024-01-01\" && status in [\"active\", \"pending\"]"
ExJexl.eval(filter, record_context)

๐Ÿ“Š Report Calculations

formula = "revenue * (1 - discount_rate) * (1 + tax_rate)"
ExJexl.eval(formula, sales_context)

๐ŸŽช Template Logic

condition = "user.role == \"admin\" || (user.department == \"IT\" && user.seniority > 2)"
ExJexl.eval(condition, template_context)

Testing

Run the test suite:

mix test

Run benchmarks:

mix run benchmark/quick_bench.exs

Comparison with Other Libraries

Feature ExJexl Plain Elixir Other JEXL
Performance ~460K ops/sec Native speed Varies
Safety Sandboxed Full access Usually safe
Syntax JEXL standard Elixir syntax JEXL variants
Learning Curve Low High Low
Dynamic Rules Excellent Complex Good
Type System JSON-compatible Rich types Limited

Contributing

Contributions are welcome! Please feel free to submit issues, feature requests, or pull requests.

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Make your changes and add tests
  4. Ensure all tests pass (mix test)
  5. Commit your changes (git commit -m 'Add amazing feature')
  6. Push to the branch (git push origin feature/amazing-feature)
  7. Open a Pull Request

License

ExJexl is released under the MIT License. See the LICENSE file for details.

Acknowledgments

Changelog

See CHANGELOG.md for a detailed history of changes.


Made with โค๏ธ for the Elixir community