# Celixir
A pure Elixir implementation of Google's [Common Expression Language (CEL)](https://cel.dev/).
CEL is a non-Turing-complete expression language designed for simplicity, speed, and safety. It is commonly used in security policies, protocol buffers, Firebase rules, and configuration validation.
## Installation
```elixir
def deps do
[
{:celixir, "~> 0.2.0"}
]
end
```
## Quick Start
```elixir
# Simple expressions
Celixir.eval!("1 + 2") # => 3
Celixir.eval!("'hello' + ' ' + 'world'") # => "hello world"
# Variable bindings
Celixir.eval!("age >= 18", %{age: 21}) # => true
# Complex expressions
Celixir.eval!(
"request.method == 'GET' && resource.public",
%{request: %{method: "GET"}, resource: %{public: true}}
)
# => true
# Comprehensions
Celixir.eval!("[1, 2, 3].filter(x, x > 1)") # => [2, 3]
Celixir.eval!("[1, 2, 3].map(x, x * 2)") # => [2, 4, 6]
Celixir.eval!("[1, 2, 3].all(x, x > 0)") # => true
```
## Compile Once, Evaluate Many
For hot paths, compile the expression once and evaluate with different bindings:
```elixir
{:ok, program} = Celixir.compile("user.role == 'admin' && request.method in ['PUT', 'DELETE']")
Celixir.Program.eval(program, %{
user: %{role: "admin"},
request: %{method: "DELETE"}
})
# => {:ok, true}
```
## Create Reusable Functions
Compile a CEL expression into a plain anonymous function you can pass around:
```elixir
validator = Celixir.to_fun!("age >= 18 && status == 'active'")
validator.(%{age: 25, status: "active"}) # => {:ok, true}
validator.(%{age: 15, status: "active"}) # => {:ok, false}
# Use in pipelines, pass to other modules, store in config
rules = %{
can_edit: Celixir.to_fun!("user.role in ['admin', 'editor']"),
is_active: Celixir.to_fun!("user.status == 'active'")
}
rules.can_edit.(%{user: %{role: "admin"}}) # => {:ok, true}
```
## Load Expressions from Files
Store CEL expressions in files for config-driven rule engines:
```elixir
# rules/access_policy.cel contains: user.role == 'admin' || resource.public
{:ok, program} = Celixir.load_file("rules/access_policy.cel")
Celixir.Program.eval(program, %{user: %{role: "viewer"}, resource: %{public: true}})
# => {:ok, true}
```
## Custom Functions
Extend CEL with your own functions written in Elixir. Custom functions receive
plain Elixir values (unwrapped from CEL internal types) and should return plain
Elixir values.
### Basic function
```elixir
env = Celixir.Environment.new(%{name: "world"})
|> Celixir.Environment.put_function("greet", fn name -> "Hello, #{name}!" end)
Celixir.eval!("greet(name)", env)
# => "Hello, world!"
```
### Multi-argument functions
```elixir
env = Celixir.Environment.new()
|> Celixir.Environment.put_function("clamp", fn val, lo, hi ->
val |> max(lo) |> min(hi)
end)
Celixir.eval!("clamp(150, 0, 100)", env)
# => 100
```
### Using module functions
```elixir
defmodule MyFunctions do
def factorial(0), do: 1
def factorial(n) when n > 0, do: n * factorial(n - 1)
end
env = Celixir.Environment.new()
|> Celixir.Environment.put_function("factorial", &MyFunctions.factorial/1)
Celixir.eval!("factorial(5)", env)
# => 120
```
### Namespaced functions
Use dot-separated names to organize functions into logical groups:
```elixir
env = Celixir.Environment.new()
|> Celixir.Environment.put_function("str.reverse", fn s ->
s |> String.graphemes() |> Enum.reverse() |> Enum.join()
end)
|> Celixir.Environment.put_function("str.repeat", fn s, n ->
String.duplicate(s, n)
end)
Celixir.eval!(~S|str.reverse("hello")|, env)
# => "olleh"
Celixir.eval!(~S|str.repeat("ab", 3)|, env)
# => "ababab"
```
### Building a function library with `defcel`
Use `Celixir.API` to define function libraries declaratively:
```elixir
defmodule MyApp.CelMath do
use Celixir.API, scope: "mymath"
defcel abs(x) do
Kernel.abs(x)
end
defcel clamp(val, lo, hi) do
val |> max(lo) |> min(hi)
end
end
env = Celixir.Environment.new() |> MyApp.CelMath.register()
Celixir.eval!("mymath.abs(-42)", env)
# => 42
Celixir.eval!("mymath.clamp(150, 0, 100)", env)
# => 100
```
Multiple API modules can be composed on the same environment:
```elixir
env =
Celixir.Environment.new(%{price: 100})
|> MyApp.CelMath.register()
|> MyApp.CelFormatting.register()
```
### Private environment data
Store opaque data on the environment for use in custom functions, without
exposing it as a CEL variable:
```elixir
env =
Celixir.Environment.new()
|> Celixir.Environment.put_private(:api_key, "secret-123")
|> Celixir.Environment.put_function("fetch", fn url ->
# api_key is accessible from Elixir but not from CEL expressions
# ...
end)
```
### Using with `Celixir.Program` (compile once, evaluate many)
```elixir
env = Celixir.Environment.new()
|> Celixir.Environment.put_function("discount", fn price, pct -> price * (1 - pct) end)
{:ok, program} = Celixir.compile("discount(price, 0.1)")
Celixir.Program.eval(program, env |> Celixir.Environment.put_variable("price", 100))
# => {:ok, 90.0}
Celixir.Program.eval(program, env |> Celixir.Environment.put_variable("price", 50))
# => {:ok, 45.0}
```
## Extensions
Celixir ships optional extension modules that mirror the `ext.*` packages from
[cel-go](https://github.com/google/cel-go/tree/master/ext). Each module exposes
a `register/1` function you pipe into your environment to activate.
```elixir
env =
Celixir.Environment.new()
|> Celixir.Ext.Math.register()
|> Celixir.Ext.Strings.register()
|> Celixir.Ext.Lists.register()
|> Celixir.Ext.Sets.register()
|> Celixir.Ext.Encoders.register()
|> Celixir.Ext.Regex.register()
```
### `Celixir.Ext.Math`
Numeric and bitwise functions under the `math.*` namespace.
```elixir
env = Celixir.Environment.new() |> Celixir.Ext.Math.register()
Celixir.eval!("math.sqrt(16.0)", env) # => 4.0
Celixir.eval!("math.ceil(1.2)", env) # => 2.0
Celixir.eval!("math.abs(-7)", env) # => 7
Celixir.eval!("math.isNaN(1.0/0.0)", env) # => false
Celixir.eval!("math.greatest(1, 3, 2)", env) # => 3
Celixir.eval!("math.least([5, 1, 3])", env) # => 1
Celixir.eval!("math.bitAnd(0b1010, 0b1100)", env) # => 8
```
| Function | Description |
|---|---|
| `math.ceil(double)` | ceiling |
| `math.floor(double)` | floor |
| `math.round(double)` | round (ties away from zero) |
| `math.trunc(double)` | truncate fractional part |
| `math.abs(int\|uint\|double)` | absolute value |
| `math.sign(int\|uint\|double)` | -1, 0, or 1 |
| `math.sqrt(int\|uint\|double)` | square root (NaN for negative) |
| `math.isNaN(double)` | true if NaN |
| `math.isInf(double)` | true if ±Inf |
| `math.isFinite(double)` | true if neither NaN nor Inf |
| `math.bitAnd(int, int)` | bitwise AND |
| `math.bitOr(int, int)` | bitwise OR |
| `math.bitXor(int, int)` | bitwise XOR |
| `math.bitNot(int)` | bitwise NOT |
| `math.bitShiftLeft(int, int)` | left shift |
| `math.bitShiftRight(int, int)` | right shift |
| `math.greatest(args...)` | variadic max |
| `math.least(args...)` | variadic min |
### `Celixir.Ext.Strings`
Additional string functions.
```elixir
env = Celixir.Environment.new() |> Celixir.Ext.Strings.register()
Celixir.eval!(~s|strings.quote("hello\nworld")|, env)
# => "\"hello\\nworld\""
```
| Function | Description |
|---|---|
| `strings.quote(string)` | wrap in double quotes with Go-style escaping |
### `Celixir.Ext.Lists`
Extra list operations and the `sortBy`/`transformMapEntry` comprehension macros.
```elixir
env = Celixir.Environment.new() |> Celixir.Ext.Lists.register()
Celixir.eval!("lists.range(5)", env) # => [0, 1, 2, 3, 4]
Celixir.eval!("[1, 2, 1, 3].distinct()", env) # => [1, 2, 3]
Celixir.eval!("[1, 2, 3].first()", env) # => optional(1)
Celixir.eval!("[1, 2, 3].last()", env) # => optional(3)
Celixir.eval!("[1, [2, [3]]].flatten(2)", env) # => [1, 2, 3]
# sortBy macro
Celixir.eval!(~s|[{"b": 2}, {"a": 1}].sortBy(x, x.key)|, env)
# transformMapEntry macro
Celixir.eval!(~s|{"a": 1, "b": 2}.transformMapEntry(k, v, {k: v * 10})|, env)
```
| Function | Description |
|---|---|
| `lists.range(n)` | `[0, 1, ..., n-1]` |
| `list.distinct()` | deduplicate preserving order |
| `list.first()` | optional first element |
| `list.last()` | optional last element |
| `list.flatten(depth)` | flatten to given depth |
| `list.sortBy(var, key_expr)` | sort by computed key (macro) |
| `map.transformMapEntry(k, v, transform [, filter])` | transform map entries (macro) |
### `Celixir.Ext.Sets`
Set-theoretic operations on lists treated as sets.
```elixir
env = Celixir.Environment.new() |> Celixir.Ext.Sets.register()
Celixir.eval!("sets.contains([1,2,3], [2,3])", env) # => true
Celixir.eval!("sets.equivalent([1,2], [2,1])", env) # => true
Celixir.eval!("sets.intersects([1,2], [2,3])", env) # => true
```
| Function | Description |
|---|---|
| `sets.contains(list, list)` | true if first list contains all elements of second |
| `sets.equivalent(list, list)` | true if lists contain the same elements (order-independent) |
| `sets.intersects(list, list)` | true if lists share at least one element |
### `Celixir.Ext.Encoders`
Base64 encoding and decoding.
```elixir
env = Celixir.Environment.new() |> Celixir.Ext.Encoders.register()
Celixir.eval!("base64.encode(b'hello')", env) # => "aGVsbG8="
Celixir.eval!("base64.decode('aGVsbG8=')", env) # => b"hello"
```
| Function | Description |
|---|---|
| `base64.encode(bytes)` | encode bytes to base64 string |
| `base64.decode(string)` | decode base64 string to bytes (error if invalid) |
### `Celixir.Ext.Regex`
Regular expression functions under the `regex.*` namespace.
```elixir
env = Celixir.Environment.new() |> Celixir.Ext.Regex.register()
Celixir.eval!(~s|regex.replace("hello world", "hello", "hi")|, env)
# => "hi world"
Celixir.eval!(~s|regex.replace("aabbcc", "[a-z]", "x", 3)|, env)
# => "xxxbcc"
Celixir.eval!(~s|regex.extract("item-A", "item-(\\w+)").value()|, env)
# => "A"
Celixir.eval!(~s|regex.extractAll("id:1, id:2", "id:\\d+")|, env)
# => ["id:1", "id:2"]
```
| Function | Description |
|---|---|
| `regex.replace(target, pattern, replacement)` | replace all matches |
| `regex.replace(target, pattern, replacement, count)` | replace first N matches (0=keep, <0=all) |
| `regex.extract(target, pattern)` | optional first match or first capture group |
| `regex.extractAll(target, pattern)` | list of all matches or capture groups |
> Use `\N` for backreferences in replacements. `$N`-style references are not supported.
## Compile-Time Sigil
Parse expressions at compile time for zero runtime parsing cost:
```elixir
import Celixir.Sigil
ast = ~CEL|request.method == "GET"|
Celixir.eval_ast(ast, %{request: %{method: "GET"}})
# => {:ok, true}
```
## Supported Features
### Types
`int`, `uint`, `double`, `bool`, `string`, `bytes`, `list`, `map`, `null`, `timestamp`, `duration`, `optional`, `type`
### Operators
- Arithmetic: `+`, `-`, `*`, `/`, `%`
- Comparison: `==`, `!=`, `<`, `<=`, `>`, `>=`
- Logical: `&&`, `||`, `!` (with short-circuit error absorption)
- Ternary: `? :`
- Membership: `in`
### Standard Functions
- **String**: `contains`, `startsWith`, `endsWith`, `matches`, `size`, `charAt`, `indexOf`, `lastIndexOf`, `lowerAscii`, `upperAscii`, `replace`, `split`, `substring`, `trim`, `join`, `reverse`
- **Math**: `math.least`, `math.greatest`, `math.ceil`, `math.floor`, `math.round`, `math.abs`, `math.sign`, `math.isNaN`, `math.isInf`, `math.isFinite`
- **Lists**: `size`, `sort`, `slice`, `flatten`, `reverse`, `lists.range`
- **Sets**: `sets.contains`, `sets.intersects`, `sets.equivalent`
- **Type conversions**: `int()`, `uint()`, `double()`, `string()`, `bool()`, `bytes()`, `timestamp()`, `duration()`, `dyn()`, `type()`
- **Encoding**: `base64.encode()`, `base64.decode()`
### Comprehension Macros
`all`, `exists`, `exists_one`, `filter`, `map`, `transformList`, `transformMap`, `sortBy`, `transformMapEntry`
### Optional Values
`optional.of()`, `optional.none()`, `optional.ofNonZeroValue()`, `.hasValue()`, `.value()`, `.orValue()`, `.or()`
### Protobuf Integration
Field access, `has()` presence checks, and automatic well-known type conversion via `Celixir.ProtobufAdapter`.
### Static Type Checking
Optional pre-evaluation type validation:
```elixir
{:ok, ast} = Celixir.parse("x + 1")
:ok = Celixir.Checker.check(ast, %{"x" => :int})
{:error, _} = Celixir.Checker.check(ast, %{"x" => :string})
```
## CEL Spec Conformance
Celixir passes 2400/2427 (99%) of the upstream [cel-spec](https://github.com/google/cel-spec) conformance tests across 30 test suites covering arithmetic, strings, lists, comparisons, logic, macros, conversions, timestamps, protobuf field access, namespaces, optionals, type deductions, and more.
The extension modules (`Celixir.Ext.*`) mirror the `ext.*` packages from cel-go and are covered by an additional 100+ tests.
## License
Apache-2.0