The Common Expression Language (CEL) is a simple expression language built on top of protocol buffer types. This page discusses basic usage. See Language Definition for the reference.
Suppose a type defined as follows (using protocol buffer syntax):
packagegoogle.account; import"google/protobuf/struct.proto"; messageAccount { oneof { stringuser_id=1; int64gaia_id=2; } string display_name = 3; string phone_number = 4; repeated string emails = 5; google.protobuf.Struct properties = 6; ... }
CEL allows for simple computations to be defined on types like this one. For example, given an instance of the Account
type assigned to the variable account
:
has(account.user_id) || has(account.gaia_id) // true if either one is set size(account.emails) > 0 // true if emails is non-empty matches(account.phone_number, "[0-9-]+") // true if number matches regexp
CEL expressions support most operators and functions one would expect when working with protocol buffers: boolean operators, relations, arithmetics on numbers, string and byte string operations, and operations to deal with lists and maps. The full reference of standard operations is described in the language definition. Note that applications of CEL may add more functions and also allow users to define their own.
CEL also supports construction of list, map, and protocol buffer objects. The following expressions evaluate to true:
Account{user_id: 'Pokemon'}.user_id == 'Pokemon' [true,false,true][1] == false {'blue': 0x000080, 'red': 0xFF0000}['red'] == 0xFF0000
CEL expressions are usually type checked, though the language is designed such that type checking is optional. Moreover, the language can deal with dynamically typed features of the most recent protocol buffer standard, proto3, like google.protobuf.Struct
. Consider the field account.properties
which has type Struct
. This type represents an untyped Object and is similar to JSON's notion of an Object. Many APIs use this type for representing user defined data which doesn't have a matching protocol buffer type. Within CEL, these objects support the same accesses and functions as protocol buffer types:
has(account.properties.id) && size(account.properties.id) > 0
When the expression account.properties
is evaluated, CEL will automatically convert the underlying Struct
representation into a map<string, google.protobuf.Value>
, on which the has
function can be called. And if a Value
is accessed, it will be automatically converted into one of the variants as expressed by its oneof-definitions. In the example, if id
is defined in the struct, and its Value
represents a string, the function size
on strings will be called. If, in turn, Value
represents a list, the function size
on lists is called.
If one views CEL coming from dynamic languages, it's natural to think of the size
function as taking any value and determining what to do with it based on the runtime type (which may include throwing an error because the type cannot be processed). Coming from statically typed languages, it's natural to think of size
as an overloaded function which, if resolution is not possible at compile time, will be deferred to runtime.
Type dependent behavior can also be expressed, since types are first-class citizens of CEL:
has(account.properties.id) && (type(account.properties.id) == string || type(account.properties.id) == list)
CEL's default type checker deals with a mixture of dynamic and static typing; the resulting type system is commonly also called gradual typing. Simply spoken, the type checker tries to figure out as much as it can, and defers undecidable decisions to the runtime. However, a CEL expression which does not make use of any of the dynamic features based on Struct
et.al can be always fully type checked at compile time.