Sentinel: Introduction to Sentinel Language – Part-3

Share At:

Telecom Review - The rise of the hybrid cloud

This Article only discusses about following topics in Sentinel Language:

  • Boolean Expressions
  • Arithmetic
  • Slices
  • Conditionals
  • Loops
  • Collection Operations

Sentinel policies are written using the Sentinel language. This language is easy to learn and easy to write. You can learn the Sentinel language and be productive within an hour. Learning Sentinel doesn’t require any formal programming experience.

Language: Boolean Expressions

Boolean expressions are expressions that evaluate to a boolean value from the result of a combination of one or more comparisons and logical operators. Boolean expressions form the basis of policy since policy can be broken down to a set of logical decisions that turn into true or false.

A single boolean expression is the body of a rule. Additionally, boolean expressions are used with conditionals, and more.

Order of Operations

The precedence of operators is shown below. Operators with higher precedence values (larger numbers) are evaluated first. Operators with the same precedence value are evaluated left-to-right.

Precedence    Operator
    6             *  /  %
    5             +  -
    4             else
    3             ==  !=  <  <=  >  >= "is" "is not" "matches" "contains" "in"
    2             and
    1             or  xor

Examples of this precedence are shown below:

4 * 5 / 5 // 4
4 * 5 + 2 // 22
4 + 5 * 2 // 14

Logical Operators

Logical operators are used to change or combine logical expressions. There are three binary operators andor, and xor. There is a single unary operator not (which can also be specified as !).

The binary operators require two boolean operands and the unary operator requires a single boolean operand. In both case, the operands are values or expressions that are boolean types.

Below is a basic explanation of the logical operators directly from the specification:

and    conditional AND    p and q  is  "if p then q else false"
or     conditional OR     p or q   is  "if p then true else q"
xor    conditional XOR    p xor q  is  "if p and not q or not p and q then true"
!      NOT                !p       is  "not p"
not    NOT                !p       is  "not p"

Using parentheses to group boolean expressions, you can combine boolean expressions to become more complex or affect ordering:

(p and q) or r
p and (q or r)

Comparison Operators

Comparison operators compare two operands and yield a boolean value.

For any comparison, the two operands must be equivalent types. The one exception are integers and floats. When an integer is compared with a float, the integer is promoted to a floating point value.

The available comparison operators are:

==       equal
!=       not equal
<        less
<=       less or equal
>        greater
>=       greater or equal
"is"     equal
"is not" not equal

The behavior of is with == and is not with != is identical.

Example comparisons:

name is "Mitchell"
idnumber > 42
idnumber <= 12

Using the logical operators, these can be combined:

name is "Mitchell" and idnumber > 42

The language specification provides more detail on exactly how comparison behaves.

Set Operators

The set operators contains and in test for inclusion in a collection (a list or map).

Set operators may be negated by prefixing the operator with not, such as not contains and not in. This is equivalent to wrapping the expression in a unary not but results in a more readable form.

contains tests if the left-hand collection contains the right-hand value. in tests if the right-hand collection contains the left-hand value. For maps, “contains” looks at the keys, not the values.

Examples:

[1, 2, 3] contains 2            // true
[1, 2, 3] contains 5            // false
[1, 2, 3] contains "value"      // false
[1, 2, 3] not contains "value"  // true

{ "a": 1, "b": 2 } contains "a"     // true
{ "a": 1, "b": 2 } contains "c"     // false
{ "a": 1, "b": 2 } contains 2       // false
{ "a": 1, "b": 2 } not contains 2   // true

Matches Operator

The matches operator tests if a string matches a regular expression. The matches operator can be negated by prefixing the operator with not, such as not matches.

The left-hand value is the string to test. The right-hand value is a string value representing a regular expression. The syntax of the regular expression is the same general syntax used by Python, Ruby, and other languages. The precise syntax accepted is the syntax accepted by RE2.

Regular expressions are not anchored by default; any anchoring must be explicitly specified.

"test" matches "e"            // true
"test" matches "^e"           // false
"TEST" matches "test"         // false
"TEST" matches "(?i)test"     // true
"ABC123" matches "[A-Z]+\\d+" // true
"test" not matches "e"        // false

Any, All Expressions

any and all expressions allow testing that any or all elements of a collection match some boolean expression. The result of an any and all expression is a boolean.

any returns the boolean value true if any value in the collection expression results in the body expression evaluating to true. If the body expression evalutes to false for all values, the any expression returns false.

all returns the boolean true if all values in the collection expression result in the body expression evaluating to true. If any value in the collection expression result in the body expression evaluating to false, the all expression returns false.

For empty collections, any returns false and all returns true.

all group.tasks as t { t.driver is "vmware" }
any group.tasks as t { t.driver is "vmware" }

Since any and all expressions are themselves boolean expressions, they can be combined with other operators:

any ["a", "b"] as char { char is "a" } or
other_value is "another"

Emptiness Comparison

The expressions is empty and is not empty provide a convenience method for determining the emptiness of a value. It supports the same types as the built-in length, collections and strings.

[] is empty            // true
[] is not empty        // false
["foo"] is empty       // false
["foo"] is not empty   // true
undefined is empty     // undefined
undefined is not empty // undefined

Language: Arithmetic

Sentinel supports arithmetic operators for integers and floats. Sentinel supports sum, difference, product, quotient, and remainder.

+    sum
*    difference
*    product
/    quotient
%    remainder

These operators work in a typical infix style:

4 + 8 // 12
8 * 2 // 16
8 / 4 // 2
8 / 5 // 1
8 % 5 // 3

Order of Operations

Arithmetic follows a standard mathematical order of operations. Grouping with parentheses can be used to affect ordering.

4 * 5 / 5 // 4
4 * 5 + 2 // 22
4 + 5 * 2 // 14
(4 + 5) * 2 // 18

A full table of operator precendence can be found on the boolean expressions page. This shows how arithmetic operators relate to other operators.

Integer Division

Integer division with a remainder rounds down to the nearest integer. Example: 8 / 3 is 2.

If the divisor is zero, an error occurs.

Mixed Numeric Operations

Mixed numeric operations between integer and floating-point values are permitted. The result is a floating-point operation with the integer converted to a floating-point value for purposes of calculation.

import "types"

a = 1.1 + 1      // 2.1
types.type_of(a) // "float"

Language: Slices

Slices are an efficient way to create a substring or sublist from an existing string or list, respectively.

The syntax for creating a slice is:

a[low : high]

low is the lowest index to slice from. The resulting slice includes the value at lowhigh is the index to slice to. The resulting slice will not include the value at high. The length of the resulting list or string is always high - low.

a = [1, 2, 3, 4, 5]
b = a[1:4] // [2, 3, 4]

a = "hello"
b = a[1:4] // "ell"

Convenience Shorthands

Some convenience shorthands are available by omitting the value for either the low or high index in the slice expression: omitting low implies a low index of 0, and omitting high implies a high index of the length of the list.

a = [1, 2, 3, 4, 5]

b = a[:2] // [1, 2]    (same as a[0:2])
b = a[2:] // [3, 4, 5] (same as a[2:length(a)])

Language: Conditionals

Conditional statements allow your policy to behave differently depending on a condition.

Conditional statements may only appear outside of rule expressions, such as in functions or in the global scope of a policy. This is because rules are only allowed to contain a single boolean expression.

If Statements

if statements only execute their bodies if a condition is met. The syntax of an if statement is:

if condition {
  // ... this is executed if condition is true
}

The condition must result in a boolean, such as by calling a function or evaluating a boolean expression. If the condition is true, the body (within the {}) is executed. Otherwise, the body is skipped.

Examples:

// This would execute the body
value = 12
if value is 18 {
    print("condition met")
}

// Direct boolean values can be used
value = true
if value {
    print("condition met")
}

// This would not execute the body since the boolean expression will
// result in undefined.
value = {}
if value["key"] > 12 {
    print("condition met")
}

Else, Else If

An else clause can be given to an if statement to execute a body in the case the condition is not met. By putting another if statement directly after the else, multiple conditions can be tested for. The syntax is:

if condition {
    // ...
} else {
    // ...
}

if condition {
    // ...
} else if other_condition {
    // ...
} else {
    // ...
}

Scoping

The body of an if statement does not create a new scope. Any variables assigned within the body of an if statement will modify the scope that the if statement itself is in.

Example:

if true {
    a = 42
}

print(a) // 42
a = 18
if true {
    a = 42
}

print(a) // 42

Case Statements

case statements are a selection control mechanism that execute a clause based on matching expressions. It is worth noting that the expression for case is optional. When no expression is provided, it defaults the expression to true. Additionally, the order of clauses is important, as they are evaluated from top to bottom, executing the first match. The syntax of a case statement is:

case expression {
    when clause_expression:
        // executed when clause_expression and expression are equal
    else:
        // executed if no clause matches expression
}

When Clause

Any clause that has an expression for comparison must use the when keyword. It accepts a list of expressions, seperated by a ,.

Example:

case x {
    when "foo", "bar":
        return true
}
case {
    when x > 40:
        return true
}

Else Clause

The else keyword allows for capturing any expressions that have no matching when clause.

Example:

case x {
    when "foo", "bar":
        return true
    else:
        return false
}

Language: Loops

Loop statements allow you to execute a body of code for each element in a collection or for some fixed number of times.

Loop statements may only appear outside of rule expressions, such as in functions or in the global scope of a policy. This is because rules are only allowed to contain a single boolean expression.

For Statements

for statements allow repeated execution of a block for each element in a collection.

Example:

// A basic sum
count = 0
for [1, 2, 3] as num {
    count += num
}

The syntax is for COLLECTION as value. This will iterate over the collection, assigning each element to value. In the example above, each element is assigned to num. The body is executed for each element. In the example above, the body adds num to the count variable. This creates a basic sum of all values in the collection.

For a map, the assigned element is the key in the map. In the example below, name would be assigned map keys.

list = []
for { "a": 1, "b": 2 } as name {
    append(list, name)
}

print(list) // ["a" "b"]

An alternate syntax is for COLLECTION as key, value. This will assign both the key and value to a variable. For a list, the key is the element index. For a map, it is the key and value is assigned the element value. Example:

count = 0
for { "a": 1, "b": 2 } as name, num {
    count += num
}

print(count) // 3

Scoping

The body of a for statement creates a new scope. If a variable is assigned within the body of a for statement that isn’t assigned in a parent scope, that variable will only exist for the duration of the body execution.

Example:

for list as value {
    a = 42
}

print(a) // undefined
a = 18
for list as value {
    a = 42
}

print(a) // 18

Language: Collection Operations

Collection operations are expressions that are performed on a list or map to return a variation of the initial data.

At the moment, filter is the only collection operation available.

Filter Expression

filter is a quantifier expression that returns a subset of the provided collection. Only elements whose filter body returns true will be returned. If any of the elements filter body returns undefined, the final result will be undefined.

Filter uses the same syntax as the any and all boolean expressions:

filter list as value { condition }      // Single-iterator, list
filter list as idx, value { condition } // Double-iterator, list

filter map as key { condition }         // Single-iterator, map
filter map as key, value { condition }  // Double-iterator, map

Examples:

l     = [1, 1, 2, 3, 5, 8]
evens = filter l as v { v % 2 is 0 } // [2, 8]

m           = { "a": "foo", "b": "bar" }
matched_foo = filter m as _, v { v is "foo" } // { "a": "foo" }

Map Expression

map is a quantifier expression that returns a list, regardless of the input collection type. Each element within the input collection is evaluted according to the map expression body and appended to the result.

l = [1, 2]
r = map l as v { v % 2 } // [false, true]

m = { "a": "foo", "b": "bar" }
r = map m as k, v { v } // ["foo", "bar"]

Happy Learning !!!


Share At:
0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
Back To Top

Contact Us