# Writing BNDRY Policies

BNDRY uses **CEL** (Common Expression Language) to power our entity risk ratings and policy engine.

**CEL** is a simple expression language that can be used to evaluate expressions in a safe and efficient way.

## Literals

| **Comment** | // (line comments only) |
|  --- | --- |
| **Boolean** | true, false |
| **Integer** | 42, -17, 0x2A (hex) |
| **Unsigned** | 42u, 0x2Au |
| **Float** | 3.14, -0.5, 6.022e23 |
| **String** | "foo", 'bar', """multiline""" |
| **Bytes** | b"data", b'data' |
| **List** | [1, 2, 3] |
| **Map** | {"a": 1, "b": 2} |
| **Null** | null |


### Strings

Strings can be enclosed in single quotes or double quotes. For multiline strings, use triple quotes (`"""` or `'''`).


```cel
"Hello\nWorld"
'Single quoted'
"""Multiline
string"""
```

Raw strings (prefixed with `r`) don't interpret escape sequences:


```cel
r"\n\t"  // literal backslash-n-backslash-t
```

### Bytes

Bytes literals are prefixed with `b`:


```cel
b"hello"
b'\xF0\x9F\xA4\xAA'  // UTF-8 encoding of 🤪
```

## Operators

| **Arithmetic** | +, -, *, /, % (modulus) |
|  --- | --- |
| **Comparison** | ==, !=, <, >, <=, >= |
| **Logical** | ! (not), && (and), || (or) |
| **Conditional** | ? : (ternary) |
| **Membership** | in |
| **Indexing** | [], . |


### Membership Operators

The `in` operator checks if an item exists in a list or a key exists in a map.


```cel
2 in [1, 2, 3]              // true
"name" in {"name": "John"}  // true
```

### Field Selection

Access fields of messages or map values with `.` or `[]`:


```cel
user.name
user["name"]
```

Access list elements with `[]`:


```cel
items[0]     // first element
items[-1]    // last element
```

### Conditional Operator

The ternary operator evaluates a condition and returns one of two values:


```cel
score >= 60 ? "pass" : "fail"
```

### Logical Operators

The `&&` and `||` operators provide commutative evaluation (may evaluate in any order):


```cel
user.admin && user.active
error || true  // returns true, ignoring error
```

For traditional short-circuit evaluation, use the ternary operator:


```cel
// Short-circuit AND
condition ? second_condition : false

// Short-circuit OR  
condition ? true : second_condition
```

## String Functions

### contains(str, substring)

Checks if a string contains a substring.


```cel
"hello world".contains("world")  // true
```

### startsWith(str, prefix)

Checks if a string starts with a prefix.


```cel
"hello world".startsWith("hello")  // true
```

### endsWith(str, suffix)

Checks if a string ends with a suffix.


```cel
"hello world".endsWith("world")  // true
```

### matches(str, regex)

Tests if a string matches a regular expression pattern (RE2 syntax).


```cel
"foobar".matches("foo.*")  // true
matches("test123", "[a-z]+[0-9]+")  // true
```

### size(str)

Returns the length of a string in Unicode code points.


```cel
"hello".size()     // 5
size("world!")     // 6
```

## List Functions

### size(list)

Returns the number of elements in a list.


```cel
[1, 2, 3].size()    // 3
size([])            // 0
```

### in operator

Checks if a value exists in a list.


```cel
2 in [1, 2, 3]      // true
5 in [1, 2, 3]      // false
```

## Map Functions

### size(map)

Returns the number of entries in a map.


```cel
{"a": 1, "b": 2}.size()  // 2
```

### in operator

Checks if a key exists in a map.


```cel
"name" in {"name": "John", "age": 30}  // true
```

## Comprehension Macros

### has(field)

Tests whether a field is set in a message or whether a key exists in a map.


```cel
has(user.address)
has(map.key_name)
```

### all(var, predicate)

Tests if all elements satisfy a condition.


```cel
[1, 2, 3].all(x, x > 0)              // true
{"a": 1, "b": 2}.all(k, k != 'c')    // true
```

### exists(var, predicate)

Tests if any element satisfies a condition.


```cel
[1, 2, 3].exists(x, x > 2)           // true
[].exists(x, x > 0)                   // false
```

### exists_one(var, predicate)

Tests if exactly one element satisfies a condition.


```cel
[1, 2, 3].exists_one(x, x == 2)      // true
[2, 2, 3].exists_one(x, x == 2)      // false
```

### map(var, transform)

Transforms each element using an expression.


```cel
[1, 2, 3].map(x, x * 2)              // [2, 4, 6]
["a", "b"].map(s, s.upperAscii())    // ["A", "B"]
```

With filter predicate:


```cel
[1, 2, 3, 4].map(x, x % 2 == 0, x * 2)  // [4, 8]
```

### filter(var, predicate)

Returns elements that satisfy a condition.


```cel
[1, 2, 3, 4].filter(x, x % 2 == 0)   // [2, 4]
{"a": 1, "b": 2}.filter(k, k == "a")  // ["a"]
```

## Timestamp and Duration Functions

### timestamp(string)

Creates a timestamp from an RFC3339 string.


```cel
timestamp("2023-08-26T12:39:00-07:00")
```

### duration(string)

Creates a duration from a string (supports h, m, s, ms, us, ns).


```cel
duration("1h30m")
duration("500ms")
duration("-1.5h")
```

### Timestamp Methods

Get components of a timestamp:


```cel
timestamp("2023-12-25T12:30:45Z").getFullYear()     // 2023
timestamp("2023-12-25T12:30:45Z").getMonth()        // 11 (December, 0-indexed)
timestamp("2023-12-25T12:30:45Z").getDate()         // 25
timestamp("2023-12-25T12:30:45Z").getHours()        // 12
timestamp("2023-12-25T12:30:45Z").getMinutes()      // 30
timestamp("2023-12-25T12:30:45Z").getSeconds()      // 45
timestamp("2023-12-25T12:30:45Z").getDayOfWeek()    // 1 (Monday)
timestamp("2023-12-25T12:30:45Z").getDayOfYear()    // 358
```

With timezone:


```cel
timestamp("2023-12-25T00:00:00Z").getDate("America/Los_Angeles")  // 24
```

### Duration Methods

Convert or extract from durations:


```cel
duration("1h30m").getHours()         // 1
duration("1h30m").getMinutes()       // 90
duration("1.234s").getMilliseconds() // 234
```

### Arithmetic with Time

Add or subtract time values:


```cel
timestamp("2023-01-01T00:00:00Z") + duration("24h")
timestamp("2023-01-10T12:00:00Z") - timestamp("2023-01-10T00:00:00Z")
duration("1h") + duration("30m")
```

## Type Conversion Functions

### int(value)

Converts to a signed integer.


```cel
int("123")                // 123
int(3.14)                 // 3
int(timestamp("2023-08-26T12:00:00Z"))  // Unix epoch seconds
```

### uint(value)

Converts to an unsigned integer.


```cel
uint("123")               // 123u
uint(3.14)                // 3u
```

### double(value)

Converts to a double.


```cel
double(10)                // 10.0
double("3.14")            // 3.14
```

### string(value)

Converts to a string.


```cel
string(123)               // "123"
string(true)              // "true"
string(b'hello')          // "hello"
string(duration("1m1ms")) // "60.001s"
```

### bytes(value)

Converts to bytes.


```cel
bytes("hello")            // b'hello'
bytes("🤪")              // b'\xF0\x9F\xA4\xAA'
```

### bool(value)

Converts to boolean.


```cel
bool("true")              // true
bool("FALSE")             // false
```

### type(value)

Returns the type of a value.


```cel
type(123)                 // int
type("hello")             // string
type([1, 2])              // list
```

### dyn(value)

Marks a value as dynamically typed (for type-checking purposes).


```cel
dyn([1, 3.14, "foo"])     // list(dyn)
```

## Bytes Functions

### size(bytes)

Returns the number of bytes.


```cel
b'hello'.size()           // 5
size(b'\xF0\x9F\xA4\xAA') // 4
```

## Common Patterns

### Safe Navigation

Check field existence before accessing:


```cel
has(user.profile) && user.profile.name == "John"
```

### List Processing


```cel
// Filter then transform
orders.filter(o, o.total > 100).map(o, o.id)

// Count matching items
users.filter(u, u.age >= 18).size()

// Check all items
items.all(item, item.quantity > 0)
```

### Map Operations


```cel
// Check if key exists
has(config.timeout)

// Get value with default
has(settings.retries) ? settings.retries : 3

// Filter map to list of keys
{"a": 10, "b": 5, "c": 20}.filter(k, map[k] > 10)  // ["c"]
```

### Time-based Logic


```cel
// Check if timestamp is in range
now > timestamp("2023-01-01T00:00:00Z") && 
now < timestamp("2024-01-01T00:00:00Z")

// Calculate age
duration(now - user.birthdate).getHours() / 24 / 365
```

### String Validation


```cel
// Email-like pattern
user.email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")

// Required fields
user.name.size() > 0 && user.email.size() > 0
```

## Error Handling

CEL expressions can produce errors at runtime. Common errors include:

- `no_matching_overload`: Function called with wrong argument types
- `no_such_field`: Accessing a non-existent field
- Division by zero
- Type conversion failures
- Overflow errors


Use logical operators to handle potential errors:


```cel
// This won't error even if division by zero occurs on left side
x / 0 || true  // returns true

// Safe field access
has(obj.field) && obj.field > 10
```

## Best Practices

1. **Use `has()` before accessing optional fields**

```cel
has(user.address) && user.address.city == "NYC"
```
2. **Prefer explicit type conversions**

```cel
int(userInput) + 5  // instead of relying on implicit conversion
```
3. **Use macros for list/map operations**

```cel
items.all(x, x.valid)  // instead of manual iteration
```
4. **Chain operations clearly**

```cel
users
  .filter(u, u.active)
  .map(u, u.email)
  .exists(e, e.endsWith("@company.com"))
```
5. **Handle null values explicitly**

```cel
user.name != null && user.name.size() > 0
```