# Writing CEL Expressions

You are writing expressions in CEL (Common Expression Language). CEL is a small, safe expression language designed for evaluating single expressions — it is NOT a general-purpose programming language. There are no statements, assignments, loops, or function definitions. Every CEL program is a single expression that evaluates to a value.

## Types

CEL has these types:

| Type | Examples | Notes |
|------|----------|-------|
| `number` | `42`, `3.14`, `0xFF` | All numbers are arbitrary-precision decimals. There is no `int`, `uint`, or `float` distinction — just `number`. `0.1 + 0.2 == 0.3` is `true`. |
| `string` | `"hello"`, `'world'` | Single or double quotes. Triple-quoted for multiline: `"""text"""`. Raw strings: `r"no\escapes"`. |
| `bool` | `true`, `false` | |
| `bytes` | `b"data"`, `b'\xff'` | Binary data. |
| `null` | `null` | |
| `list` | `[1, 2, 3]` | Ordered collection. |
| `map` | `{"key": "value"}` | Key-value pairs. |
| `Date` | `date("2023-01-15")` | Backed by TZDate. Supports any parseable date string. |

## Operators

### Arithmetic (number only)
```cel
1 + 2          // 3
10 - 3         // 7
4 * 5          // 20
10 / 3         // 3.333...
10 % 3         // 1
-x             // negation
```

### Comparison
```cel
a == b         // equality (works on all types, deep comparison for lists/maps)
a != b         // inequality
a < b          // less than
a <= b         // less or equal
a > b          // greater than
a >= b         // greater or equal
```

### Logical
```cel
a && b         // AND (short-circuits)
a || b         // OR (short-circuits)
!a             // NOT
```

### Ternary
```cel
condition ? value_if_true : value_if_false
```

### Membership
```cel
"a" in ["a", "b", "c"]       // true — element in list
"key" in {"key": "value"}    // true — key in map
```

### String concatenation
```cel
"hello" + " " + "world"    // "hello world"
```

### List concatenation
```cel
[1, 2] + [3, 4]            // [1, 2, 3, 4]
```

### Spread (`...expr`)
A spread inlines another list inside a list literal or another map inside a
map literal. Order is preserved — a later entry (including a later spread)
overrides an earlier one when keys collide.
```cel
[1, ...a, 4]                 // a flattens in position
[...a, ...b]                 // equivalent to a + b

{...a, "x": 1}               // copy a, then set x to 1
{"x": 1, ...a}               // set x, then let a override
{...a, ...b}                 // b overrides a on overlapping keys
{...a, "x": 1, ...b}         // a, then x, then b — last write wins

[...[1, 2], 3, ...[4]]       // [1, 2, 3, 4]
```
Spread is only valid as a list element or map entry — `f(...args)` is not
supported (CEL resolves overloads by fixed arity). Spreading a non-list into
a list, or a non-map into a map (including `null`), is an error — at compile
time when statically known, at runtime when the source is `dyn`.

### Field access
```cel
obj.field                   // access field on map/object
obj["field"]                // bracket access (equivalent)
obj.?field                  // optional field access (returns optional)
obj[?0]                     // optional index access (returns optional)
```

## Built-in Functions

### Type conversions
```cel
number("42")               // parse string to number
number(true)               // 1 (false gives 0)
string(42)                 // "42"
string(true)               // "true"
bool("true")               // true (also accepts "t", "1", "TRUE", "True")
bool("false")              // false (also accepts "f", "0", "FALSE", "False")
bytes("text")              // UTF-8 encode to bytes
int(3.7)                   // 3 (truncates to integer)
double("3.14")             // alias for number()
type(x)                    // returns the type of x
dyn(x)                     // identity, marks as dynamic type
```

### Size
```cel
size("hello")              // 5 (Unicode-aware character count)
size([1, 2, 3])            // 3
size({"a": 1})             // 1
size(b"data")              // 4
// Also available as method:
"hello".size()             // 5
[1, 2].size()              // 2
```

### List aggregations
```cel
[1, 2, 3].sum()                       // 6
[1.5, 2.5].sum()                      // 4
items.map(i, i.price).sum()           // total of mapped prices
```
Receiver must be statically typed `list<number>`. Works on number literals and `.map(...)` results; an unlisted variable typed as `dyn` will not match — wrap or type it first.

### String methods
```cel
"hello".contains("ell")              // true
"hello".startsWith("hel")            // true
"hello".endsWith("llo")              // true
"HELLO".lowerAscii()                 // "hello"
"hello".upperAscii()                 // "HELLO"
"  hi  ".trim()                      // "hi"
"hello".substring(1)                 // "ello"
"hello".substring(1, 3)              // "el"
"hello".indexOf("l")                 // 2
"hello".lastIndexOf("l")             // 3
"hello".matches("[a-z]+")            // true (regex)
"a,b,c".split(",")                   // ["a", "b", "c"]
"a,b,c".split(",", 2)               // ["a", "b,c"]
["a", "b"].join()                    // "ab"
["a", "b"].join(", ")               // "a, b"
```

### Bytes methods
```cel
b"hello".string()                    // "hello" (UTF-8 decode)
b"\xff".hex()                        // "ff"
b"data".base64()                     // "ZGF0YQ=="
b'{"a":1}'.json()                    // {"a": 1}
b"hello".at(0)                       // 104
size(b"hello")                       // 5
```

### JSON
```cel
JSON.parse('{"a":1,"b":[2,3]}')      // dyn value: {"a": 1, "b": [2, 3]}
JSON.parse("42")                     // 42
JSON.stringify({"a": 1, "b": [2,3]}) // '{"a":1,"b":[2,3]}'
JSON.stringify(1 + 2)                // "3"
```
`JSON` is a built-in namespace — you don't import it, it's always available.
`JSON.parse` accepts any JSON string and returns a `dyn` value.
`JSON.stringify` takes any value and serializes it; CEL-internal shapes are
unwrapped first so the output is plain JSON — numbers (CEL's `Decimal`) become
JS numbers, optionals become their value (or `null` when empty), and `Date`
values become ISO 8601 strings.

### FormData
```cel
// Build multipart/form-data for an upload:
http.post("https://api.example.com/files")
  .body(form({"file": myFile, "name": "Alice", "tags": ["a", "b"]}))
```
`form(map): FormData` turns a flat record into a native `FormData`. Pass it
straight to `.body(...)` — `fetch` sets the multipart boundary, so you don't
need a `Content-Type` header.

Value coercion is flat (same rules as `.query(...)`):
- `null` / `undefined` / absent `.?` values → key is skipped
- `Option.Some(x)` → unwrapped to `x`
- `File` / `Blob` → appended as-is (the `File` filename is preserved)
- numbers / booleans / dates → coerced to string (`Date` → ISO 8601)
- arrays → one entry per element under the same key
- nested objects / maps → error (multipart is flat — use `JSON.stringify(...)`
  on the value if you need a structured field)

### Dates
```cel
date("2023-06-15")                   // create a Date
date("2023-06-15T10:30:00Z")        // with time component
date(1718409600)                     // from a Unix timestamp — seconds...
date(1718409600000)                  // ...or milliseconds (unit auto-detected by magnitude)

// Comparisons:
date("2023-01-01") < date("2024-01-01")   // true
date("2023-01-01") == date("2023-01-01")  // true

// Start/end of period:
d.startOf("day")                     // start of day
d.startOf("week")                    // start of week
d.startOf("month")                   // start of month
d.startOf("year")                    // start of year
d.endOf("month")                     // end of month

// Offset (positive or negative):
d.offset(3, "days")                  // 3 days later
d.offset(-1, "month")               // 1 month earlier
d.offset(2, "weeks")                // 2 weeks later
d.offset(1, "year")                 // 1 year later

// Set specific fields:
d.set("day", 15)                     // set day of month to 15
d.set("month", 6)                    // set to June (1-based)
d.set("year", 2025)                  // set year to 2025

// Format:
d.format("yyyy-MM-dd")              // "2023-06-15"
d.format("MMMM d, yyyy")           // "June 15, 2023"
```

### Ranges
`range()` builds a list. It is **end-exclusive** (Rust `..` style) and the
direction is taken from the operands — when `start > end` it counts down.
```cel
range(0, 5)                          // [0, 1, 2, 3, 4]
range(0, 10, 2)                      // [0, 2, 4, 6, 8] — with a step
range(5, 0)                          // [5, 4, 3, 2, 1] — counts down
range(date("2024-01-01"), date("2024-04-01"), "month")
                                     // [2024-01-01, 2024-02-01, 2024-03-01]
// Date unit is one of: "year", "month", "day", "hour", "minute".
```

### URL
A `URL` value is supplied by the host (e.g. a `location` variable bound to the
current page URL). You cannot construct one in CEL.
`query(name)` returns `optional<string>` — `Some(value)` when the param is
present (including a present-but-empty value), `None` when it is absent. Unwrap
it with `.orValue(...)` / `.hasValue()` / `.value()`.
```cel
location.query("tab").orValue("")    // value of the ?tab= search param, "" if absent
location.query("missing").hasValue() // false — absent params are None
location.query("id").orValue("")     // first value when a param repeats
```

## HTTP

Build HTTP requests using the `http` builder. A CEL expression only *describes* a request — it always evaluates to an `HttpRequest`, never issues it. The host decides when and how to send it (downloading a file, reading a JSON response, etc.). There is no way to perform the request or read its response from CEL.

**URLs must be absolute** — pass a full `https://…` URL. There is no base URL, so relative paths like `/api/users` will not resolve.

### Creating requests
```cel
http.get("https://api.example.com/users")        // GET request
http.post("https://api.example.com/users")       // POST request
http.put("https://api.example.com/users/1")      // PUT request
http.patch("https://api.example.com/users/1")    // PATCH request
http.delete("https://api.example.com/users/1")   // DELETE request
```

### Builder methods (chainable)
```cel
// Add headers:
http.get("https://api.example.com/users")
  .header("Authorization", "Bearer token")
  .header("Accept", "application/json")

// Add query parameters:
http.get("https://api.example.com/users")
  .query("page", "1")
  .query("limit", "10")

// Splat a record (e.g. a form) into query parameters.
// null, undefined and "" values are skipped; arrays append one entry per item;
// nested objects throw.
http.get("https://api.example.com/users")
  .query(form)
  .query("page", string(pageIndex))

// Set request body. An object serializes to JSON; set the Content-Type
// header yourself. Optional values are unwrapped — a present `.?` field
// keeps its value, an absent one becomes `null` (the key is kept):
http.post("https://api.example.com/users")
  .header("Content-Type", "application/json")
  .body({"name": "Alice", "channel": query.?channel})

// The expression always evaluates to an HttpRequest — the host sends it
// (downloading a File, reading a JSON response, etc.):
http.post("https://api.example.com/users")
  .header("Content-Type", "application/json")
  .header("Authorization", "Bearer " + token)
  .body({"name": "Alice"})
```

## Optionals

An `optional<T>` is either present (`Some`) or absent (`None`). They come from
`.?` / `[?]` access, `find`, and `URL.query`. Build them with `optional.of(x)`
(the lift — wraps a value as present) and `optional.none()`.

```cel
optional.of(5)                          // present, holds 5
optional.none()                         // absent

o.hasValue()                            // bool — is it present?
o.value()                               // unwrap — ERRORS if absent
o.orValue(default)                      // unwrap, or `default` when absent
o.or(otherOptional)                     // first present optional wins
```

### map — transform a present value
`o.map(v, body)` binds the present value to `v`, evaluates `body` (a plain value),
and re-wraps the result as present, only when `o` is present; an absent `o`
short-circuits to `None` and `body` is never evaluated.
```cel
loc.query("id").map(v, "user-" + v).orValue("anon")
// "user-42" when ?id=42 is present; "anon" when absent
```

### flatMap — chain a computation that may itself be absent
`o.flatMap(v, body)` is like `map`, but `body` must itself return an `optional`
(so it can decide the result is absent). Use `flatMap` when the next step can
fail; use `map` when it always produces a value.
```cel
loc.query("id").flatMap(v, optional.of("user-" + v)).orValue("anon")
// same as the map example above

optional.of(2).flatMap(v, optional.none()).hasValue()   // false — body collapsed it
```
Together with `optional.of`, `flatMap` is a lawful monad and `map` its derived
Functor (`o.map(v, body)` == `o.flatMap(v, optional.of(body))`).

## Macros

Macros are special forms — they look like method calls but have special evaluation semantics.

### map — Transform each element
```cel
[1, 2, 3].map(x, x * 2)                    // [2, 4, 6]
[1, 2, 3, 4].map(x, x > 2, x * 10)         // [30, 40] (filter then transform)
```

### filter — Keep elements matching predicate
```cel
[1, 2, 3, 4, 5].filter(x, x > 3)           // [4, 5]
```

### all — True if ALL elements match
```cel
[1, 2, 3].all(x, x > 0)                    // true
[1, -1, 3].all(x, x > 0)                   // false (short-circuits)
```

### exists — True if ANY element matches
```cel
[1, 2, 3].exists(x, x > 2)                 // true
[1, 2, 3].exists(x, x > 5)                 // false
```

### exists_one — True if EXACTLY one element matches
```cel
[1, 2, 3].exists_one(x, x > 2)             // true (only 3)
[1, 2, 3].exists_one(x, x > 1)             // false (2 and 3 both match)
```

### find — Return first element matching predicate as optional
```cel
[1, 2, 3].find(x, x > 1)                   // optional.of(2)
[1, 2, 3].find(x, x > 5)                   // optional.none()
[1, 2, 3].find(x, x > 1).orValue(0)        // 2
[1, 2, 3].find(x, x > 5).orValue(0)        // 0
```

### has — Check if a field exists
```cel
has(obj.field)                               // true if field exists
has(obj.nested.deep)                         // safe nested check
```

### cel.bind — Bind a variable to avoid repeated computation
```cel
cel.bind(total, price * quantity, total > 100 && total < 1000)
```

## Common Patterns

### Conditional logic
```cel
age >= 18 ? "adult" : "minor"
```

### Null-safe access
```cel
has(user.address) && user.address.city == "NYC"
```

### List searching
```cel
items.exists(item, item.status == "active")
items.filter(item, item.price > 100).size() > 0
```

### String matching
```cel
email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")
```

### Numeric comparisons
```cel
price * quantity >= 100.0
values.all(v, v >= 0 && v <= 100)
```

### Map membership
```cel
"admin" in user.roles
```

## Things You Cannot Do

- **No variables or assignments** — `x = 5` is invalid. CEL is a single expression.
- **No loops** — Use `map`, `filter`, `all`, `exists` instead.
- **No function definitions** — You can only call built-in or registered functions.
- **No statements** — No `if`/`else` blocks, `for`, `while`, `return`. Use the ternary `? :` for conditionals.
- **No mutation** — You cannot modify values. Every operation produces a new value.
- **No `int` or `uint` type** — All numbers are just `number`. Write `42`, not `int(42)`. You do not need to worry about integer vs float precision — `0.1 + 0.2 == 0.3` works correctly.
- **No imports or modules** — Everything available is already in scope.

## Error Cases to Avoid

- **Division/modulo by zero** — `10 / 0` and `10 % 0` throw errors.
- **Index out of bounds** — `[1,2,3][5]` throws. Use `has()` or check `size()` first.
- **Missing fields** — `obj.nonexistent` throws. Use `has(obj.nonexistent)` to check first.
- **Type mismatches** — `1 + "2"` throws. Types must match for operators.
- **Invalid regex** — `"x".matches("[invalid")` throws. Ensure regex is valid.
