Skip to content

Latest commit

 

History

History
99 lines (81 loc) · 4.05 KB

intro.md

File metadata and controls

99 lines (81 loc) · 4.05 KB

Introduction

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):

package google.account;

import "google/protobuf/struct.proto";

message Account {
  oneof {
    string user_id = 1;
    int64 gaia_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 definiton. 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.