CEL Expressions
expr
expressions use the Common Expression Language (CEL)
The CEL playground lets you quickly test CEL expressions
---
apiVersion: canaries.flanksource.com/v1
kind: Canary
metadata:
name: currency-converter-display-cel
spec:
http:
- name: USD
url: https://api.frankfurter.app/latest?from=USD&to=GBP,EUR,ILS,ZAR
display:
expr: "'$1 = €' + string(json.rates.EUR) + ', £' + string(json.rates.GBP) + ', ₪' + string(json.rates.ILS)"
Values in CEL represent any of the following:
Type | Description |
---|---|
int | 64-bit signed integers |
uint | 64-bit unsigned integers |
double | 64-bit IEEE floating-point numbers |
bool | Booleans (true or false ) |
string | Strings of Unicode code points |
bytes | Byte sequences |
list | Lists of values |
map | Associative arrays with int , uint , bool , or string keys |
null_type | The value null |
message names | Protocol buffer messages |
type | Values representing the types in the first column |
Handling null types and missing keys
When dealing with CEL objects, we might get errors where a key does not exist or if you're chaining keys, then one of the keys in the middle will be missing
// Assume we have an obj with value: {'a': {'b': 'c'}}
o.a.b => c
// But this will yield an error
o.a.d // Error, attribute 'd' does not exist
// To handle these, we can use the `or` or `orValue` directives
o.a.?d.orValue('fallback value') => 'fallback value'
// And if d exists, its value is returned
You can read more about or and orValue below
aws
aws.arnToMap
Takes in an AWS arn and parses it and returns a map.
aws.arnToMap("arn:aws:sns:eu-west-1:123:MMS-Topic") //
// map[string]string{
// "service": string,
// "region": string,
// "account": string,
// "resource": string,
// }
aws.fromAWSMap
aws.fromAWSMap
takes a list of map[string]string
and merges them into a single map. The input map is expected to have the field "Name".
aws.fromAWSMap(x).hello" == "world" // `true`
// Where
// x = [
// { Name: 'hello', Value: 'world' },
// { Name: 'John', Value: 'Doe' },
// ];
base64
base64.encode
base64.encode
encodes the given byte slice to a Base64 encoded string.
base64.decode("aGVsbG8=") // return b'hello'
base64.decode
base64.decode
decodes the given base64 encoded string back to its original form.
base64.decode("aGVsbG8=") // return b'hello'
collections
.keys
The keys
method on a map returns a list of keys.
Syntax:
e.keys()
// Where:
// `e` is a map .
Examples:
{"first": "John", "last": "Doe"}.keys() // ["first", "last"]
.merge
The merge
method on a map takes a second map to merge.
Syntax:
e.merge(x)
// Where:
// `e` is the map you want to merge to.
// `x` is the second map to merge from.
Examples:
{"first": "John"}.merge({"last": "Doe"}) // {"first": "John", "last": "Doe"}
.omit
The omit
method on a map takes a list of keys to remove.
Syntax:
e.omit(x)
// Where:
// `e` is a list .
// `x` is a list of keys to omit.
Examples:
{"first": "John", "last": "Doe"}.omit(["first"]) // {"last": "Doe"}
.sort
The sort
method on a list returns a sorted list.
Syntax:
e.sort()
// Where:
// `e` is a list .
Examples:
[3, 2, 1].sort() // [1, 2, 3]
['c', 'b', 'a'].sort() // ['a', 'b', 'c']
.uniq
The uniq
method on a list returns a list of unique items.
Syntax:
e.uniq()
// Where:
// `e` is a list .
Examples:
[1,2,3,3,3,].uniq().sum() // 10
["a", "b", "b"].uniq().join() // "ab"
.values
The values
method on a map returns a list of items.
Syntax:
e.values()
// Where:
// `e` is a map .
Examples:
{'a': 1, 'b': 2}.values().sum() // 3
all
The all
macro tests whether a predicate holds for all elements of a list e
or keys of a map e
. It returns a boolean value based on the evaluation.
If any predicate evaluates to false, the macro evaluates to false, ignoring any errors from other predicates
Syntax:
e.all(x, p)
// Where:
// `e` is the list or a map.
// `x` represents each element of the list.
// `p` is the condition applied to each entry.
Examples:
// Checking if all elements of a list are greater than 0:
[1, 2, 3].all(e, e > 0) // true
// Ensure that the all the map keys begin with the letter "a"
{"a": "apple", "b": "banana", "c": "coconut"}.all(k, k.startsWith("a")) // false
exists
The exists
macro checks if there is at least one element in a list that satisfies a given condition. It returns a boolean value based on the evaluation.
Syntax:
e.exists(x, p)
// Where:
// `e` is the list you're checking.
// `x` represents each element of the list.
// `p` is the condition applied to each entry.
Example:
// Checking if any element of a list is equal to 2:
[1, 2, 3].exists(e, e == 2) // true
exists_one
The exists_one
macro checks if there is exactly one element in a list that satisfies a given condition. It returns a boolean value based on the evaluation.
Syntax:
e.exists_one(x, p)
// Where:
// `e` is the list you're checking.
// `x` represents each element of the list.
// `p` is the condition applied to each entry.
Example:
[1, 2, 3].exists_one(e, e > 1) // false
[1, 2, 3].exists_one(e, e == 2) // true
filter
The filter
macro creates a new list containing only the elements or entries of an existing list that satisfy the given condition.
Syntax:
e.filter(x, p)
Where:
e
is the list you are filtering.x
represents each element of the list.p
is the predicate expression applied to each entry.
Examples:
// Filtering a list to include only numbers greater than 2:
[1, 2, 3, 4].filter(e, e > 2) // [3, 4]
fold
The fold
macro combines all elements of a collection, such as a list or a map, using a binary function. It's a powerful tool for aggregating or reducing data.
Syntax:
//For lists:
list.fold(e, acc, <binary_function>)
//For maps:
map.fold(k, v, acc, <binary_function>)
Where:
list
is the list you're folding.map
is the map you're folding.e
represents each element of the list.k
represents each key of the map.v
represents each value of the map.acc
is the accumulator, which holds the intermediate results.<binary_function>
is the function applied to each entry and the accumulator.
Examples:
[1, 2, 3].fold(e, acc, acc + e) // 6
// Concatenating all values of a map:
{"a": "apple", "b": "banana"}.fold(k, v, acc, acc + v) // "applebanana"
has
The has
macro tests whether a field is available. It's particularly useful for protobuf messages where fields can be absent rather than set to a default value. It's especially useful for distinguishing between a field being set to its default value and a field being unset. For instance, in a protobuf message, an unset integer field is indistinguishable from that field set to 0 without the has
macro.
Syntax
has(x.y): boolean
// Where
// `x` is a message or a map and
// `y` (string) is the field you're checking for.
Example:
If you have a message person
with a potential field name
, you can check for its presence with:
has(person.name) // true if 'name' is present, false otherwise
in
The membership test operator checks whether an element is a member of a collection, such as a list or a map. It's worth noting that the in
operator doesn't check for value membership in maps, only key membership.
Syntax:
"apple" in ["apple", "banana"] // => true
3 in [1, 2, 4] // => false
map
The map
macro creates a new list by transforming a list e
by taking each element x
to the function given by the expression t
, which can use the variable x
.
Syntax:
e.map(x, t)
// Where:
// `e` is the list you are transforming.
// `x` represents each element of the list.
// `t` is the transformation function applied to each entry.
e.map(x, p, t)
// Where:
// `e` is the list you're transforming.
// `p` filter before the value is transformed
// `x` represents each element of the list.
// `t` is the transformation function applied to each entry.
Examples:
// Transforming each element of a list by multiplying it by 2:
[1, 2, 3].map(e, e * 2) // [2, 4, 6]
[(1, 2, 3)].map(x, x > 1, x + 1) // [3, 4]
or
If the value on the left-hand side is none-type, the optional value on the right hand side is returned. If the value on the left-hand set is valued, then it is returned. This operation is short-circuiting and evaluates as many links in the or
chain as are needed to return a non-empty optional value.
obj.?field.or(m[?key]) // if obj.field is none, m.key is returned
l[?index].or(obj.?field.subfield).or(obj.?other) // first non-none of l[index], obj.field.subfield, obj.other is returned
orValue
When creating map or list, if a field may be optionally set based on its presence, then placing a ?
before the field name or key ensures the type on the right-hand side must be optional(T) where T is the type of the field or key-value.
// The following returns a map with the key expression set only if the subfield is present, otherwise a default value is returned
{'a': 'x', 'b': 'y', 'c': 'z'}.?c.orValue('empty') // Returns z since `c` exists
{'a': 'x', 'b': 'y'}.?c.orValue('empty') // Returns 'empty' since `c` doesn't exist
// We can use the same for list types
[1, 2, 3][?2].orValue(5) // Return 3 since 2nd index has a value
[1, 2][?2].orValue(5) // Return 5 since 2nd index doesn't exist
size
size
determines the number of elements in a collection or the number of Unicode characters in a string.
Syntax
(string) -> int string length
(bytes) -> int bytes length
(list(A)) -> int list size
(map(A, B)) -> int map size
"apple".size() // 5
b"abc".size() // 3
["apple", "banana", "cherry"].size() // 3
{"a": 1, "b": 2}.size(); // 2
slice
Returns a new sub-list using the indices provided.
[1, 2, 3, 4].slice(1, 3) // return [2, 3]
[(1, 2, 3, 4)].slice(2, 4) // return [3 ,4]
sets
sets.contains
Returns whether the first list argument contains all elements in the second list argument. The list may contain elements of any type and standard CEL equality is used to determine whether a value exists in both lists. If the second list is empty, the result will always return true.
sets.contains(list(T), list(T)) -> bool
Examples:
sets.contains([], []) // true
sets.contains([], [1]) // false
sets.contains([1, 2, 3, 4], [2, 3]) // true
sets.contains([1, 2.0, 3u], [1.0, 2u, 3]) // true
sets.equivalent
Returns whether the first and second list are set equivalent. Lists are set equivalent if for every item in the first list, there is an element in the second which is equal. The lists may not be of the same size as they do not guarantee the elements within them are unique, so size doesn't factor into the computation.
sets.equivalent(list(T), list(T)) -> bool
Examples:
sets.equivalent([], []) // true
sets.equivalent([1], [1, 1]) // true
sets.equivalent([1], [1u, 1.0]) // true
sets.equivalent([1, 2, 3], [3u, 2.0, 1]) // true
sets.intersects
Returns whether the first list has at least one element whose value is equal to an element in the second list. If either list is empty, the result is false.
sets.intersects([1], []) // false
sets.intersects([1], [1, 2]) // true
sets.intersects(
[[1], [2, 3]],
[
[1, 2],
[2, 3.0]
]
) // true
csv
CSV
CSV
converts a CSV formatted array into a two-dimensional array, where each element is a row string.
CSV(["Alice,30", "Bob,31"])[0][0] // "Alice"
crypto
crypto.SHA1|256|384|512
The crypto.SHA*
functions are used to compute the SHA hash of the input data.
crypto.SHA1("hello") // "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c"
crypto.SHA256("hello") // "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
dates
timestamp
timestamp
represent a point in time. It's typically used in conjunction with other functions to extract or manipulate time-related data.
// Creating a timestamp for January 1st, 2023:
timestamp("2023-01-01T00:00:00Z")
// Creating another timestamp:
timestamp("2023-07-04T12:00:00Z")
.getDate
getDate
extract the date part from a timestamp. It returns a string representation of the date.
// Extracting the date from a timestamp:
"2023-01-01T12:34:56Z".getDate() // "2023-01-01"
// Getting the date from another timestamp:
"2023-07-04T00:00:00Z".getDate() // "2023-07-04"
.get[DatePart]
Function | Description | Example |
---|---|---|
{date>.getDayOfMonth() | A integer value representing the day of the month, with the first day being 1. | 1 - 31 |
<date>.getDayOfWeek() | eturns an integer value representing the day of the week, where Sunday is 0 and Saturday is 6. | 0 - 6 |
<date>.getDayOfYear() | an integer value representing the day of the year, with January 1st being day 1. | 1 - 366 |
<date>.getDayOfMonth() | the full year (4 digits for 4-digit years) of the specified timestamp. | |
<date>.getHours() | the full year (4 digits for 4-digit years) of the specified timestamp. | 0- 23 |
<date>.getMilliseconds() | 0 -999 | |
<date>.getMinutes() | ||
<date>.getMonth() | 0 -11 | |
<date>.getSeconds() | 0 - 59 | 0 - 59 |
<date>.getHours() |
duration
duration
parses a string into a new duration.
The format is an integer followed by a unit: s
for seconds, m
for minutes, h
for hours, and d
for days.
// Creating a duration of 5 hours:
duration("5h") // Represents a duration of 5 hours
duration("30m") // Represents a duration of 30 minutes
Durations can also be crated using arithmetic:
Field | Description |
---|---|
time.Unix(epoch) | converts a UNIX time (seconds since the UNIX epoch) into a time.Time object |
time.Nanosecond | converts to a time.Duration |
time.Microsecond | |
time.Millisecond | |
time.Second | |
time.Minute | |
time.Hour |
time.ZoneName
time.ZoneName
returns the name of the local system's time zone. It doesn't require any parameters and is useful for retrieving the time zone information.
// Retrieving the local time zone name:
time.ZoneName() // Might evaluate to "PST" if the local time zone is Pacific Standard Time
time.ZoneOffset
time.ZoneOffset
returns the offset of the local system's time zone in minutes. It helps in understanding the time difference between the local time zone and UTC.
// Getting the time zone offset:
time.ZoneOffset() // Could evaluate to -480 for PST
time.Parse
time.Parse
parse a given string into a time object based on a specified layout. It's handy for converting string representations of time into actual time objects.
Syntax:
time.Parse(layout, value)
layout
is the time layout string.value
is the string representation of the time to be parsed.
// Parsing a time string with a specific format:
time.Parse("2006-01-02", "2023-09-26") // a time object representing September 26, 2023
// Another example with a different format:
time.Parse("02-01-2006", "26-09-2023") // the same time object as above
// Parsing a time with hour and minute information:
time.Parse("15:04 02-01-2006", "14:30 26-09-2023") // Includes time of day information