
This Article only discusses about following topics in Sentinel Language:
- Variables
- Values
- Lists
- Maps
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.
Simplest Example
The example below is about the simplest practical example of Sentinel. It is reasonable to imagine this as a realistic policy. This shows that in most cases, Sentinel will be extremely simple:
main = rule { request.method is "GET" and request.headers contains "X-Key" }
Files
Sentinel policies are single files that end in the .sentinel
file extension. There is currently no built-in mechanism to Sentinel for merging multiple files. This is purposefully done to make Sentinel policies easy to submit to systems that support Sentinel policies.
Ordering
Sentinel policies are executed top-down. For example:
a = 1 // a = 1 here
b = a + 1 // b = 2 here
a = 3 // a = 3, b = 2
In this example, the value of a
and b
is shown at each line. Since Sentinel executes values top-down, the final value of a
is 3 and b
is 2. b
does not become 4.
Main
Sentinel expects there to be a main
rule. The value of this rule is the result of the entire policy.
The result of the policy depends on the evaluated contents of the main
rule. For booleans, a policy passes on a true
value, and fails on a false
value. Other types generally follow a zero or zero-length pattern for determining success.
Type | Passing Condition | Failing Condition |
---|---|---|
Boolean | true | false |
String | "" | Any non-zero length string |
Integer | 0 | Any non-zero value |
Float | 0.0 | Any non-zero value |
List | [] | Any non-zero length list |
Map | {} | Any non-zero length map |
A value of main that falls outside of the above types will result in a policy error. As a special case, if main
evaluates to an undefined value, the error message will indicate as such, with a reference to where the undefined value was encountered.
More Complex Example
The simple example above is a full working example. In our experience with Sentinel, many policies can be representing using this simple form. However, to show more features of the language, a more complex example is shown below. This example is also a realistic example of what Sentinel may be used for.
import "units"
memory = func(job) {
result = 0
for job.groups as g {
for g.tasks as t {
result += t.resources.memory else 0
}
}
return result
}
main = rule {
memory(job) < 1 * units.gigabyte
}
Language: Variables
Variables store values that can be used later.
Most notably, a variable assignment of main
is required for all Sentinel policies. This is the main rule that is executed to determine the result of a policy. The main
rule may use other variables that are assigned in the policy.
Example:
a = 1
b = a + 1
In this example, two variables are assigned: a
and b
. a
is given the value of “1” and b
uses the a
to calculate its own value.
Syntax
The syntax for assigning a variable is:
IDENTIFIER = VALUE
On the left of the equal sign is an identifier. This is a name for your variable and will be how you reference it later. An identifier is any combination of letters and digits, but must start with a letter. An underscore _
is also a valid letter.
On the right is any valid Sentinel value or expression. A value is a literal value such as a number or string. An expression is some computed value such as doing math, calling a function, etc.
Assignment and Reassignment
A variable is assigned when it is given a value. You can also reassign variables at any time by setting it to a new value. The new value takes effect for any subsequent use of that variable. A variable can be reassigned to a different type.
For example:
a = 1 // a = 1
b = a // b = 1
a = "value" // a = "value", b = 1
c = a // c = "value", b = 1
In the above example you can see that the variables are set and reassigned. Notice that the value of a variable is the current value, and that reassigning a variable only affects future uses of that variable. You can see this with c
and b
being different values.
Unassigned Variables
Using a variable that is unassigned is an error.
In the example below, the first line would result in the policy erroring. Sentinel is executed top-down and the value of c
is not available yet on the first line.
a = c // Error!
c = 1
Type Conversion
Variables can be assigned (or-reassigned) a different value type via type conversion.
s = "1.1"
a = int(s) // a = 1
a = float(s) // a = 1.1
a = 1
s = string(a) // s = "1"
Language: Values
Values are the data that Sentinel policies operate on. You can create this data yourself, for example by just typing the number 42
, or you may access this data from an external source to make policy decisions.
Values have a type, which determines what kind of operatins can be performed on the data. Example types are booleans (true and false), numbers, and strings (text).
This section documents all the available value types.
Boolean
A boolean is a value that is either true or false.
A boolean value is created literally with true
or false
.
As a policy language, booleans are central to the behavior of Sentinel. Booleans are used as conditions in if statements, are the result of rules, and more. The ultimate result of a Sentinel policy is true or false.
Integer
An integer is a whole number.
An integer is a 64-bit value. This means it can represent numbers from -9,223,372,036,854,775,808 to 9,223,372,036,854,775,808.
Integers are created by typing them out literally with no separators (such as a comma). An optional prefix can set a non-decimal base: 0
for octal, and 0x
for hexadecimal. In hexadecimal literals, letters a-f
and A-F
represent values 10 to 15.
Example integers are shown below:
42
0600
0xBadFace
170141183460469
Integers are used for math and numerical comparison.
Float
A float is a number with a decimal point. It has an integer part, a decimal point, a fractional part, and optionally an exponent part. Example floats are shown below:
0.
72.40
072.40 // == 72.40
2.71828
1.e+0
6.67428e-11
1E6
.25
.12345E+5
Floats are IEEE-754 64-bit floating point numbers.
String
Strings are text values.
Strings are created by wrapping text in double quotes, such as "hello"
. Within the quotes, any character may appear except newline and an unescaped double quote. The text between the quotes is the value.
Because Sentinel policies must be UTF-8 encoded text, strings themselves are UTF-8 encoded text. This means you can put any value UTF-8 character into a string, such as "日本語"
.
You can have a string with a literal double-quote by escaping it with a backslash. For example: "they said \"hello\""
turns into the value they said "hello"
.
Backslash escapes can be used for much more than only escaping a double quote. Newlines are \n
, a literal backslash is \\
, and many more
Example strings:
`abc` // same as "abc"
`\n
\n` // same as "\\n\n\\n"
"\n"
"\"" // same as `"`
"Hello, world!\n"
"日本語"
"\u65e5本\U00008a9e"
"\xff\u00FF"
"\uD800" // illegal: surrogate half
"\U00110000" // illegal: invalid Unicode code point
Strings support indexing. Indexing a string will access that byte in the string as if the string were a byte array. This is an important distinction: it will not access the character at that position if you’re using multi-byte characters.
Strings support slicing to efficiently create substrings.
Type Conversion
The built-in functions int
, float
, string
, and bool
convert a value to a value of that type. Some examples are shown below followed by a list of exact rules for type conversion.
int(42) // 42
int("42") // 42
int(42.8) // 42
int(true) // 1
float(1.2) // 1.2
float(1) // 1.0
float("4.2") // 4.2
float(true) // 1.0
string("foo") // "foo"
string(88) // "88"
string(0xF) // "15"
string(true) // "true"
bool("true") // true
bool(1) // true
bool(-1) // true
bool(0.1) // true
bool("false") // false
bool(0) // false
For int
:
- Integer values are unchanged
- String values are converted according to the syntax of integer literals
- Float values are rounded down to their nearest integer value
- Boolean values are converted to
1
fortrue
, and0
forfalse
For float
:
- Float values are unchanged
- Integer values are converted to the nearest equivalent floating point value
- String values are converted according to the syntax of float literals
- Boolean values are converted to
1.0
fortrue
, and0.0
forfalse
For string
:
- String values are unchanged
- Integer values are converted to the base 10 string representation
- Float values are converted to a string formatted
xxx.xxx
with a precision of 6. This is equivalent to%f
for C’s sprintf. - Boolean values are converted to
"true"
fortrue
, and"false"
forfalse
For bool
:
- The following string values convert to
true
:"1"
,"t"
,"T"
,"TRUE"
,"true"
, and"True"
- The following string values convert to
false
:"0"
,"f"
,"F"
,"FALSE"
,"false"
, and"False"
- Any non-zero integer or float value converts to
true
- Any zero integer or float value converts to
false
For any other unspecified type, the result is the undefined
value.
Language: Lists
Lists are a collection of zero or more values.
Lists can be created using by wrapping values in []
and separating them by commas. An optional trailing comma is allowed. List elements can be differing types. Examples:
[] // An empty list
["foo"] // Single element list
["foo", 1, 2, true] // Multi element list with different types
["foo", [1, 2]] // List containing another list
A list can be sliced to create sublists. The set operators can be used to test for value inclusion in a list.
Accessing Elements
List elements can be accessed with the syntax name[index]
where index
is zero-indexed.
A negative index accesses the list in reverse. It is the same as reversing a list and then using a positive index. Similar to a positive index, it is bounded by the length of the list as a negative value.
Accessing beyond the length of the list results in undefined.
Examples:
a = ["foo", 1, true, [1, 2]]
a[0] // "foo"
a[2] // true
a[4] // undefined
a[-2] // true
a[-4] // "foo"
a[-5] // undefined
a[3][1] // 2
List Append
Values can be appended to a list using the built-in append function.
This modifies the list in-place and returns undefined. For more information on why append
behaves this way, please read the full documentation for the append function.
append([1,2], 3) // [1, 2, 3]
append([1,2], "foo") // [1, 2, "foo"]
append([1,2], [3]) // [1, 2, [3]]
append(1, 3) // error()
List Concatenation
Two lists can be concatenated using the +
operator or the shorthand +=
assignment operator. For the +
operator, a new list is returned. For +=
, the left-hand list is modified in place.
Examples:
[1] + [2] // [1, 2]
[1] + [[1]] // [1, [1]]
[1] + 1 // error
a = [1]
a += [2] // a = [1, 2]
a += 3 // error
List Length
The length of a list can be retrieved using the length function.
Examples:
length([]) // 0
length(["foo"]) // 1
Removing Items From a List
You can use a combination of list concatenation and slices to remove elements from a list.
a = [1, 2, 3, 4, 5]
a = a[:2] + a[3:] // [1, 2, 4, 5]
The shorthand shown here is effectively the same as a[0:2] + a[3:length(a)]
, which creates a new list out of the concatenation two sub-lists composed of the first two elements, and the rest of the list starting at index 3. This effectively removes the 3rd element from the list (index 2).
List Comparison
Lists may be compared for equality. Lists are equal if they are of equal length and their corresponding elements are comparable and equal. Lists with the same elements but in a different order are not equal.
[1, 2] is [1, 2] // true
[1, 2] is [2, 1] // false
["a"] is ["a", "b"] // false
["a", ["b", "c"]] is ["a", ["b", "c"]] // true
List comparison speed is O(N), meaning that the speed of the comparison is linearly proportional to the number of elements in the list. The more elements, the more iterations that are necessary to verify equality.
The N-value quoted above should account for the sum of the elements of all lists in the subjects of comparison, as list comparison will recurse into these lists to check for equality.
Language: Maps
Maps are a collection of zero or more key/value pairs. These are useful for storing values that are looked up by a unique key.
Maps can be created using {}
and specifying key/value pairs. Keys and values can be differing types. An optional trailing comma is allowed. Examples:
// Empty map
{}
// Map with a single value on one line
{ "key": "value" }
// Map with multiple values with differing types on multiple lines
{
"key": "value",
42: true,
}
Maps are unordered. When looping over a map, the key/value pairs can be returned in any order.
The set operators can be used to test for key inclusion in a map.
Accessing Elements
Map elements can be accessed with the syntax name[key]
. This looks up a value by a key.
Accessing a key that doesn’t exist results in undefined.
Examples:
map = { "key": "value", 42: true, }
map["key"] // "value"
map[42] // true
map[0] // undefined
Modifying or Adding Elements
Elements can be added or modified in a map by assigning to name[key]
. If the key doesn’t exist, the value is added. If the key already exists, the value is overridden.
map = { "key": "value" }
map[42] = true // Add a new key/value
map["key"] = 12 // Modify the value of "key"
Deleting Elements
An element can be deleted from a map using the delete function.
Examples:
map = { "key": "value" }
delete(map, "key") // map is now empty
delete(map, "other") // no effect for non-existent key
Keys and Values
The keys and values of a map can be retrieved as lists using the keys and values functions.
Because maps are unordered, the keys and values are returned in an unspecified order. It should not be assumed that keys and values will be returned in a consistent order.
data = { "a": 2, "b": 3 }
keys(data) // ["b", "a"]
values(data) // [2, 3]
Map Comparison
Maps may be compared for equality. Maps are equal if they are of equal length and both their corresponding keys and values are comparable and equal.
{"foo": "bar"} is {"foo": "bar"} // true
{"foo": "bar"} is {"baz": "bar"} // false
{"foo": "bar"} is {"foo": "baz"} // false
{"foo": "bar"} is {"foo": "bar", "baz": "qux"} // false
{1: "a"} is {1.0: "a"} // true (int/float comparable)
// also true (maps are not ordered):
{"m": {"a": "b"}, "l": ["a"]} is {"l": ["a"], "m": {"a": " b"}}
Your point of view caught my eye and was very interesting. Thanks. I have a question for you.
Can you be more specific about the content of your article? After reading it, I still have some doubts. Hope you can help me.
I have read your article carefully and I agree with you very much. So, do you allow me to do this? I want to share your article link to my website: gate.io
I agree with your point of view, your article has given me a lot of help and benefited me a lot. Thanks. Hope you continue to write such excellent articles.
thank you