This guide covers the common task of registering your own function so it can be called from CEL source.
CEL itself is a standard; this guide only covers how cel_ex exposes it. For
the language, see the CEL specification.
For evaluating expressions, see Evaluating CEL expressions.
Declare and register
A function is a CEL.Function with one or more CEL.Function.Overloads.
Each overload carries its argument and result types plus an optional :eval
target. The target must be a {Module, function} tuple; closures and
anonymous functions are rejected, so environments stay introspectable.
alias CEL.Function
alias CEL.Function.Overload
defmodule MyHandlers do
# Receives the list of evaluated CEL.Value arguments.
def greet([{:string, name}]), do: {:ok, {:string, "Hello, " <> name}}
end
function = %Function{
name: "greet",
overloads: [
%Overload{
id: "greet_string",
args: [:string],
result: :string,
eval: {MyHandlers, :greet}
}
]
}
env = CEL.Env.build(functions: [function])
{:ok, value} = CEL.eval(~s|greet("world")|, %{}, env)
# => {:ok, {:string, "Hello, world"}}Call styles and generic signatures
:modes says how expressions call the function: :global (greet(x), the
default), :receiver (x.greet(), with x as the first argument) or
{:namespace, "ns"} (ns.greet(x)). Argument and result types are
CEL.Type shorthand; {:param, "T"} is a type parameter that must be the
same type everywhere it appears:
%Function{
name: "first",
modes: [:receiver],
overloads: [
%Overload{
id: "first_list",
args: [{:list, {:param, "T"}}],
result: {:param, "T"},
eval: {MyHandlers, :first}
}
]
}
# [1, 2].first() + 1 type-checks as intResults
Results come back as tagged CEL.Value terms (so int/uint and
string/bytes stay distinct). Call CEL.Value.to_native/1 when you just
want a plain Elixir value:
CEL.Value.to_native(value)
# => "Hello, world"The eval contract
The target receives one argument: the list of evaluated
CEL.Valuearguments. For receiver-mode calls (x.greet(y)) the target value is prepended to that list.It must return
{:ok, value} | {:unknown, reason} | {:error, reason}, withvalueaCEL.Valueof the declared result type. Nothing checks the value against that type, so build it with the constructors: an{:enum, "pkg.Enum"}result isCEL.Value.enum(:"pkg.Enum", number), named as the environment'sCEL.TypeProvidernames it (by atom, forPB.CEL.types/1), which is also how enum arguments arrive.{:unknown, reason}makes the call's result unknown;reasonis any term and is reported in theCEL.Unknownentry{{:call, name, expr_id}, reason}.The function is not called when an argument is an error or unknown; the call returns that error or unknown instead. Set
strict: falseon the overload to be called anyway: such arguments then arrive as{:error, %CEL.Error{}}and{:unknown, %CEL.Unknown{}}, and returning one of them passes it through unchanged. A non-strict function that turns an unknown into a value decides the result without that input, so the result no longer reflects that the input was unknown.An overload's
:eval_contextlists extra arguments appended after the argument list.:varspasses the variable bindings given to eval, as a map keyed by string name, so a function can read results fetched before evaluation:defmodule Labels do def label([{:string, name}], vars) do case Map.fetch(vars, "labels") do {:ok, {:unknown, _reason} = unknown} -> unknown {:ok, labels} -> {:ok, {:bool, name in labels}} :error -> {:unknown, :labels_not_fetched} end end end %Overload{ id: "label_string", args: [:string], result: :bool, eval: {Labels, :label}, eval_context: [:vars] }:callpasses%{function: name, overload_id: id, expr_id: id}, naming the function, the overload the checker chose and the call site, so one handler can serve several registered functions.nameincludes the namespace for namespaced calls (ns.fn).expr_idis the call's expression id, the same oneCEL.Checked.references/1reports for the call andCEL.Unknownuses in{:call, name, expr_id}, so results computed per call site before evaluation can be looked up by it. Context arguments follow in the order the list gives them:defmodule Rules do def call([{:string, user}], %{function: "is_" <> label}), do: {:ok, {:bool, Labels.has?(user, label)}} end for label <- ["spam", "bot"] do %Function{ name: "is_" <> label, overloads: [ %Overload{ id: "is_" <> label <> "_string", args: [:string], result: :bool, eval: {Rules, :call}, eval_context: [:call] } ] } end:typespasses the environment'sCEL.TypeProvider,{module, state}, so a function can read the fields of a message argument:defmodule Fields do def field([{:message, message, payload}, {:string, name}], {types, state}) do with {:ok, field} <- types.find_field(state, message, name), {:ok, value} <- types.get_field(state, payload, field) do {:ok, value} else _error -> {:error, "no such field #{name}"} end end endRegistration validates only the MFA shape (both elements must be atoms). It does not check that the module/function exists, so a missing
Mod.fun/arityat dispatch time raises:undefto the caller.An overload with no
:evalMFA is a check-only declaration; evaluating such a call raises an evaluator error.