Celixir (Celixir v0.3.0)

Copy Markdown View Source

A pure Elixir implementation of Google's Common Expression Language (CEL).

CEL is a non-Turing-complete expression language designed for simplicity, speed, and safety. It is commonly used in security policies, protocol buffers, and configuration validation.

Quick Start

iex> Celixir.eval("1 + 2")
{:ok, 3}

iex> Celixir.eval("name.startsWith('hello')", %{name: "hello world"})
{:ok, true}

iex> Celixir.eval("x > 10 ? 'big' : 'small'", %{x: 42})
{:ok, "big"}

Compile Once, Evaluate Many

{:ok, program} = Celixir.compile("x * 2 + y")

Celixir.Program.eval(program, %{x: 5, y: 1})   # => {:ok, 11}
Celixir.Program.eval(program, %{x: 10, y: 3})   # => {:ok, 23}

Create Reusable Functions

validator = Celixir.to_fun!("age >= 18 && status == 'active'")

validator.(%{age: 25, status: "active"})   # => {:ok, true}
validator.(%{age: 15, status: "active"})   # => {:ok, false}

Supported Features

  • Types: int, uint, double, bool, string, bytes, list, map, null, timestamp, duration, optional, type
  • Operators: arithmetic (+, -, *, /, %), comparison (==, !=, <, <=, >, >=), logical (&&, ||, !), ternary (?:), membership (in)
  • String functions: contains, startsWith, endsWith, matches, size, charAt, indexOf, lastIndexOf, lowerAscii, upperAscii, replace, split, substring, trim, join, reverse
  • Math functions: math.least, math.greatest, math.ceil, math.floor, math.round, math.abs, math.sign, math.isNaN, math.isInf, math.isFinite
  • List functions: size, sort, slice, flatten, reverse, lists.range
  • Set functions: sets.contains, sets.intersects, sets.equivalent
  • Comprehension macros: all, exists, exists_one, filter, map
  • Type conversions: int(), uint(), double(), string(), bool(), bytes(), timestamp(), duration(), dyn(), type()
  • Optional values: optional.of(), optional.none(), optional.ofNonZeroValue(), .hasValue(), .value(), .orValue(), .or()
  • Encoding: base64.encode(), base64.decode()
  • Custom functions: register your own via Celixir.Environment.put_function/3 or declaratively with Celixir.API and defcel
  • Reusable functions: to_fun/1 compiles to a plain anonymous function
  • File loading: load_file/1 loads expressions from files
  • Value encoding: encode/1 converts Elixir values to CEL internal types
  • Protobuf integration: field access, has() checks, well-known type conversion via Celixir.ProtobufAdapter
  • Static type checking: optional pre-evaluation validation via Celixir.Checker
  • Compile-time sigil: ~CEL|expr| for zero-cost parsed ASTs

Custom Functions

Register Elixir functions to call from CEL expressions. Functions receive plain Elixir values and should return plain Elixir values.

# Simple function
env = Celixir.Environment.new(%{name: "world"})
      |> Celixir.Environment.put_function("greet", fn name -> "Hello, #{name}!" end)

Celixir.eval("greet(name)", env)
# => {:ok, "Hello, world!"}

# Multi-argument
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)
# => {:ok, 100}

# Module function reference
env = Celixir.Environment.new()
      |> Celixir.Environment.put_function("factorial", &MyMath.factorial/1)

# Namespaced functions (dot-separated names)
env = Celixir.Environment.new()
      |> Celixir.Environment.put_function("str.reverse", &MyString.reverse/1)
      |> Celixir.Environment.put_function("str.repeat", &MyString.repeat/2)

To build a reusable function library, group registrations in a module:

defmodule MyApp.CelLibrary do
  def register(env \\ Celixir.Environment.new()) do
    env
    |> Celixir.Environment.put_function("slugify", &slugify/1)
    |> Celixir.Environment.put_function("format.currency", &format_currency/2)
  end

  defp slugify(s), do: s |> String.downcase() |> String.replace(~r/[^a-z0-9]+/, "-")
  defp format_currency(amount, cur), do: "#{cur} #{:erlang.float_to_binary(amount / 1.0, decimals: 2)}"
end

Summary

Functions

Compiles a CEL expression into a reusable Celixir.Program.

Encodes a plain Elixir value into CEL representation.

Parses and evaluates a CEL expression with optional variable bindings.

Parses and evaluates a CEL expression, raising on error.

Evaluates a pre-parsed AST with the given environment or bindings map.

Loads a CEL expression from a file and compiles it into a Celixir.Program.

Like load_file/1 but raises on error.

Parses a CEL expression string into an AST.

Compiles a CEL expression and returns a callable function.

Like to_fun/1 but raises on parse error.

Functions

compile(expression)

@spec compile(String.t()) :: {:ok, Celixir.Program.t()} | {:error, String.t()}

Compiles a CEL expression into a reusable Celixir.Program.

Parse once, evaluate many times with different bindings.

Examples

{:ok, program} = Celixir.compile("x > threshold")
Celixir.Program.eval(program, %{x: 100, threshold: 50})

encode(list)

Encodes a plain Elixir value into CEL representation.

This is the inverse of unwrap/1. Since CEL types are now native Elixir types, this is mostly an identity function — integers, strings, floats, booleans, lists, and maps pass through unchanged.

Examples

iex> Celixir.encode(42)
42

iex> Celixir.encode("hello")
"hello"

iex> Celixir.encode([1, 2, 3])
[1, 2, 3]

iex> Celixir.encode(:optional_none)
%Celixir.Types.Optional{has_value: false}

eval(expression, bindings \\ %{})

@spec eval(String.t(), map() | Celixir.Environment.t()) ::
  {:ok, any()} | {:error, String.t()}

Parses and evaluates a CEL expression with optional variable bindings.

Returns {:ok, result} on success or {:error, message} on failure. Results are unwrapped from internal tagged types to plain Elixir values.

Examples

iex> Celixir.eval("1 + 2")
{:ok, 3}

iex> Celixir.eval("x > 0", %{x: 5})
{:ok, true}

iex> Celixir.eval("undefined_var")
{:error, "undefined variable: undefined_var"}

eval!(expression, bindings \\ %{})

@spec eval!(String.t(), map() | Celixir.Environment.t()) :: any()

Parses and evaluates a CEL expression, raising on error.

Examples

iex> Celixir.eval!("2 * 3")
6

eval_ast(ast, env)

@spec eval_ast(Celixir.AST.expr(), Celixir.Environment.t() | map()) ::
  {:ok, any()} | {:error, String.t()}

Evaluates a pre-parsed AST with the given environment or bindings map.

load_file(path)

@spec load_file(String.t()) :: {:ok, Celixir.Program.t()} | {:error, String.t()}

Loads a CEL expression from a file and compiles it into a Celixir.Program.

Examples

{:ok, program} = Celixir.load_file("path/to/rule.cel")
Celixir.Program.eval(program, %{x: 42})

load_file!(path)

@spec load_file!(String.t()) :: Celixir.Program.t()

Like load_file/1 but raises on error.

parse(expression)

@spec parse(String.t()) :: {:ok, Celixir.AST.expr()} | {:error, String.t()}

Parses a CEL expression string into an AST.

The AST can be evaluated later with eval_ast/2 or stored for reuse.

Examples

iex> {:ok, ast} = Celixir.parse("1 + 2")
iex> Celixir.eval_ast(ast, %{})
{:ok, 3}

to_fun(expression)

@spec to_fun(String.t()) ::
  {:ok, (map() -> {:ok, any()} | {:error, String.t()})} | {:error, String.t()}

Compiles a CEL expression and returns a callable function.

The returned function takes a bindings map (or Celixir.Environment) and returns {:ok, result} or {:error, message}.

Examples

iex> fun = Celixir.to_fun!("x * 2 + y")
iex> fun.(%{x: 5, y: 1})
{:ok, 11}

iex> fun = Celixir.to_fun!("name.startsWith('hello')")
iex> fun.(%{name: "hello world"})
{:ok, true}

to_fun!(expression)

@spec to_fun!(String.t()) :: (map() -> {:ok, any()} | {:error, String.t()})

Like to_fun/1 but raises on parse error.

Examples

iex> fun = Celixir.to_fun!("x + 1")
iex> fun.(%{x: 10})
{:ok, 11}