The Common Expression Language (CEL): parse, type-check and evaluate CEL expressions against variables and messages.

env = CEL.Env.build(vars: [age: :int, tags: {:list, :string}])

CEL.eval("age >= 18 && 'vip' in tags", %{age: 30, tags: ["vip"]}, env)
#=> {:ok, {:bool, true}}

An expression goes through three steps, each also available on its own:

eval/3 does all three for a one-off evaluation.

Messages

CEL's own types are always there, including the protobuf well-known types it builds in: google.protobuf.Timestamp and Duration, the wrappers, Any, Value, ListValue and Struct. Message and enum types come from a CEL.TypeProvider, given to CEL.Env.build/1 as :types. For protobuf messages, PB.CEL.types/1 is one:

env =
  CEL.Env.build(
    types: PB.CEL.types(MyApp.Schema),
    container: "my.pkg",
    vars: [user: {:message, "my.pkg.User"}]
  )

CEL.eval("user.age >= 18", %{user: %{age: 30}}, env)
#=> {:ok, {:bool, true}}

Values

Variables are bound by name (atom or string keys) to plain Elixir terms, which are converted using the variable's declared type: integers, floats, strings, booleans, lists and maps, and DateTime for :timestamp. A variable of a message or enum type takes what its provider converts, for PB.CEL.types/1 a message as PB.decode/4 returns it and an enum value's name as an atom or its number. nil binds a wrapper variable to null.

Results are CEL.Value.t/0 terms, tagged so that CEL's distinctions survive: {:int, 1} and {:uint, 1}, {:string, "a"} and {:bytes, "a"}, {:enum, :"pkg.Enum", 2}, with message and enum types named as their provider names them. CEL.Value.to_native/1 turns one back into a plain term.

Results

Evaluation returns {:ok, value}, {:error, %CEL.Error{}}, or {:unknown, %CEL.Unknown{}} when the result depends on a variable bound to {:unknown, reason} or on a custom function that returned one; see CEL.Unknown. Parse and check errors are CEL.Errors too, with the line and column of the problem in the source.

Custom functions

Register CEL.Function structs with CEL.Env.build/1's :functions option. See the "Custom CEL functions" guide.

Summary

Types

A parsed expression: a cel.expr.ParsedExpr map with :expr and :source_info, as parse/2 returns.

Functions

Checks CEL source or a parsed expression into a CEL.Checked value.

Checks CEL source or a parsed expression, raising on parse or check errors.

Checks CEL source or a parsed expression and bundles the result with the supplied environment as a CEL.Program.

Checks CEL source or a parsed expression and bundles it with env, raising on parse or check errors.

Evaluates CEL source against env with the supplied variable bindings.

Evaluates CEL source against env, raising on parse, check, or eval errors and on unknown results.

Parses CEL source into a cel.expr.ParsedExpr map.

Parses CEL source into a cel.expr.ParsedExpr map, raising on errors.

Reads name.f1.f2... from values as evaluating that expression with env would, but tells an unset field apart from one set to its default.

Types

parsed()

@type parsed() :: %{expr: map(), source_info: map()}

A parsed expression: a cel.expr.ParsedExpr map with :expr and :source_info, as parse/2 returns.

Functions

check(source_or_parsed, env \\ Env.empty())

@spec check(String.t() | parsed(), CEL.Env.t()) ::
  {:ok, CEL.Checked.t()} | {:error, CEL.Error.t()}

Checks CEL source or a parsed expression into a CEL.Checked value.

The second argument must be a CEL.Env.

Passing a pre-parsed expression skips parser resource limits and assumes the AST came from parse/2 or another trusted producer.

check!(source_or_parsed, env \\ Env.empty())

@spec check!(String.t() | parsed(), CEL.Env.t()) :: CEL.Checked.t()

Checks CEL source or a parsed expression, raising on parse or check errors.

compile(source_or_parsed, env \\ Env.empty())

@spec compile(String.t() | parsed(), CEL.Env.t()) ::
  {:ok, CEL.Program.t()} | {:error, CEL.Error.t()}

Checks CEL source or a parsed expression and bundles the result with the supplied environment as a CEL.Program.

Programs carry their environment so eval callers do not have to re-pass types or function declarations on every call. See CEL.Program.

compile!(source_or_parsed, env \\ Env.empty())

@spec compile!(String.t() | parsed(), CEL.Env.t()) :: CEL.Program.t()

Checks CEL source or a parsed expression and bundles it with env, raising on parse or check errors.

eval(source, values \\ %{}, env \\ Env.empty())

@spec eval(String.t(), keyword() | map(), CEL.Env.t()) ::
  {:ok, CEL.Value.t()} | {:unknown, CEL.Unknown.t()} | {:error, CEL.Error.t()}

Evaluates CEL source against env with the supplied variable bindings.

This is the one-shot convenience that parses, checks, and evaluates source in a single call. To compile once and evaluate many times, build a CEL.Program with compile/2 and evaluate it with CEL.Program.eval/2.

The evaluator returns tagged CEL.Value terms so CEL runtime distinctions such as int/uint and string/bytes are preserved.

A variable bound to {:unknown, reason} is unknown. When the result depends on unknown inputs, the result is {:unknown, %CEL.Unknown{}}; see CEL.Unknown.

eval!(source, values \\ %{}, env \\ Env.empty())

@spec eval!(String.t(), keyword() | map(), CEL.Env.t()) :: CEL.Value.t()

Evaluates CEL source against env, raising on parse, check, or eval errors and on unknown results.

parse(source, opts \\ [])

@spec parse(
  String.t(),
  keyword()
) :: {:ok, parsed()} | {:error, CEL.Error.t()}

Parses CEL source into a cel.expr.ParsedExpr map.

Options

  • :location - where the source came from, such as a file name. Parse and check errors report it in CEL.Error's :location. Defaults to "<input>".
  • :macros - whether to expand the standard macros (has, all, exists, exists_one, map, filter, cel.bind, optMap, optFlatMap). Defaults to true.

Raises CEL.OptionError for an unknown option or an invalid value.

parse!(source, opts \\ [])

@spec parse!(
  String.t(),
  keyword()
) :: parsed()

Parses CEL source into a cel.expr.ParsedExpr map, raising on errors.

select(env, values, name, path)

@spec select(CEL.Env.t(), keyword() | map(), atom() | String.t(), [String.t()]) ::
  {:ok, CEL.Value.t()}
  | :absent
  | {:unknown, CEL.Unknown.t()}
  | {:error, CEL.Error.t()}

Reads name.f1.f2... from values as evaluating that expression with env would, but tells an unset field apart from one set to its default.

Returns :absent when has() is false at some step of the path: a field with presence that is unset (for a proto3 field without presence, one that holds its default), or a map key that is missing. Otherwise returns what evaluation would. path is a list of field names or map keys, as in the {:var, name, path} arguments of CEL.Checked.references/1; name must be a variable declared in env.

env = CEL.Env.build(vars: [req: {:map, :string, :string}])

CEL.select(env, %{req: %{"a" => ""}}, "req", ["a"])
#=> {:ok, {:string, ""}}

CEL.select(env, %{req: %{}}, "req", ["a"])
#=> :absent