# Rhai stdlib reference Everything in this document is callable from any user script without imports — Rhai's built-in standard library plus the seven PiCloud utility modules added in v1.1.0. Stateful service modules (KV, docs, HTTP, …) ship in subsequent v1.1.x releases and are documented separately. For the architectural shape (why some modules are stateless and register at engine build, why others are per-call), see [sdk-shape.md](sdk-shape.md). ## Conventions - **Throw on failure.** Every function throws a Rhai runtime error on bad input (invalid pattern, invalid encoding, out-of-range arg). Use `try { ... } catch (e) { ... }` if you want to handle it. - **`()` for absent.** Functions that semantically may have no result (e.g. `regex::find` when nothing matches) return `()`. Test with `if v == () { ... }`. - **`bool` for predicates.** Yes/no questions return `bool`. - **UTC, milliseconds, lowercase hex, RFC 3986.** Defaults chosen once, not per call. --- ## Rhai built-ins (free with every script) These come with the Rhai engine itself. See the [Rhai book](https://rhai.rs/book/lib/index.html) for full signatures. **Math:** `+ - * / %`, `min`, `max`, `abs`, `sqrt`, `pow`, `floor`, `ceil`, `round`, `to_int`, `to_float`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `exp`, `ln`, `log`, `PI()`, `E()`. **String:** `len`, `is_empty`, `contains`, `starts_with`, `ends_with`, `index_of`, `split`, `trim`, `to_lower`, `to_upper`, `replace`, `chars`, `pad`, `sub_string`, `crop`, `+` (concatenation). > **Footgun — several string methods mutate in place and return `()`.** > A whole family of Rhai string methods edit the receiver *in place* and > return unit, **not** a new string. `let t = auth.replace("Bearer ", "")` > (or `let t = text.trim()`) sets `t` to `()`, so a downstream > `text.split(",")` or `users::verify(t)` fails with > `Function not found: split (())` / `… (())`. > > | Method | Returns | Use it as | > |---|---|---| > | `replace`, `trim`, `make_upper`, `make_lower`, `crop`, `truncate`, `pad` | `()` — **mutates in place** | a statement: `text.trim();` then read `text` | > | `to_upper`, `to_lower`, `sub_string`, `split`, `chars` | a **new value** (string / array / range) | an expression: `let u = name.to_upper();` | > > So `to_upper`/`to_lower` are the *copying* variants (safe in `let x = …`), > while `make_upper`/`make_lower` are the in-place ones. To trim and keep > the result, mutate then read the same binding: > ```rhai > let token = auth; > token.trim(); // mutate in place > if token.starts_with("Bearer ") { // now use `token` > token.replace("Bearer ", ""); // again: statement, not `let x = …` > } > ``` > Or, to strip a known prefix without mutation, slice: > ```rhai > let token = if auth.starts_with("Bearer ") { auth.sub_string(7) } else { auth }; > ``` > (Verified against the pinned Rhai `=1.24` build.) **Array:** `push`, `pop`, `shift`, `insert`, `remove`, `len`, `clear`, `truncate`, `extend`, `filter`, `map`, `reduce`, `reduce_rev`, `find`, `find_map`, `any`, `all`, `index_of`, `contains`, `sort`, `reverse`, `dedup`, `chunks`, `splice`, `[]` indexing. **Map:** `len`, `is_empty`, `contains`, `keys`, `values`, `mixin`, `remove`, `clear`, `fill_with`, `+` (merge), `[]` and `.` access. **Blob:** `len`, `push`, `pop`, `clear`, `as_string`, `parse_le_int`, `write_*`, `[]` indexing. Blobs are `Vec` at the Rust layer. **Logging:** `log::trace`, `log::info`, `log::warn`, `log::error` — each takes a message and optionally a structured-data map. (Documented with the SDK contract; mentioned here for completeness.) --- ## `regex::` — regular expressions Linear-time, no backtracking (powered by the Rust `regex` crate). Patterns compile per call. | Function | Description | |---|---| | `regex::is_match(pattern, text) -> bool` | Whether `text` contains a match. | | `regex::find(pattern, text) -> String \| ()` | First match or `()` if none. | | `regex::find_all(pattern, text) -> Array` | All matches as `String` array. | | `regex::replace(pattern, text, replacement) -> String` | Replace first match only. | | `regex::replace_all(pattern, text, replacement) -> String` | Replace every match. | | `regex::split(pattern, text) -> Array` | Split `text` on matches. | | `regex::captures(pattern, text) -> Array \| ()` | `[full, group1, group2, ...]` from the first match; unmatched optional groups appear as `()`. | Invalid patterns throw. Use `\\` to escape inside Rhai string literals (`"\\d+"`) or backtick strings to skip escaping (`` `\d+` ``). ```rhai if regex::is_match(`^/api/v\d+/`, ctx.request.path) { let cap = regex::captures(`/api/v(\d+)/(.+)`, ctx.request.path); let version = cap[1]; // "1" let rest = cap[2]; // "users" } ``` --- ## `random::` — cryptographically-secure randomness All randomness comes from `OsRng`. There is deliberately no "fast non-crypto" variant — scripts shouldn't have to pick. | Function | Description | |---|---| | `random::int(min, max) -> i64` | Uniform integer in `[min, max]` (inclusive). Throws if `min > max`. | | `random::float() -> f64` | Uniform float in `[0.0, 1.0)`. | | `random::bytes(n) -> Blob` | `n` random bytes. `n` in `0..=65536`. | | `random::string(n) -> String` | `n` random alphanumeric chars (`A-Za-z0-9`). `n` in `0..=4096`. | | `random::uuid() -> String` | UUID v4 in canonical 8-4-4-4-12 form. | ```rhai let token = random::uuid(); let salt = random::bytes(16); let pin = random::int(100000, 999999); ``` --- ## `time::` — UTC time Canonical time value is **milliseconds since the Unix epoch** as `i64`. ISO 8601 / RFC 3339 strings are for I/O. UTC only — no timezone support. | Function | Description | |---|---| | `time::now() -> String` | Current UTC time as ISO 8601 with ms (e.g. `"2026-05-30T20:15:00.123Z"`). | | `time::now_ms() -> i64` | Current ms since Unix epoch. | | `time::parse(iso) -> i64` | Parse RFC 3339 / ISO 8601 string to ms. Throws on bad input. | | `time::format(ms) -> String` | Format ms-since-epoch as ISO 8601 with ms precision. | | `time::add_seconds(ms, secs) -> i64` | `ms + secs*1000`, with overflow check. | | `time::diff_seconds(a_ms, b_ms) -> i64` | `(b_ms - a_ms) / 1000`, truncated. | ```rhai let started_at = time::now_ms(); // ... do work ... let elapsed = time::diff_seconds(started_at, time::now_ms()); let deadline = time::format(time::add_seconds(time::now_ms(), 3600)); ``` --- ## `json::` — JSON parse and stringify | Function | Description | |---|---| | `json::parse(s) -> Dynamic` | Parse a JSON string. Returns Rhai maps, arrays, scalars, or `()` for null. Throws on invalid JSON. | | `json::stringify(v) -> String` | Compact JSON. | | `json::stringify_pretty(v) -> String` | Pretty-printed (2-space indent). | ```rhai let payload = json::parse(ctx.request.body); // if body came in as a string let body_str = json::stringify(#{ ok: true, items: [1, 2, 3] }); ``` Note: `ctx.request.body` is *already* parsed when the request body is `Content-Type: application/json` — only call `json::parse` on raw strings. --- ## `base64::` — standard and URL-safe Base64 Two alphabets: standard (with `=` padding) and URL-safe (no padding). Encoders accept both `String` and `Blob`; decoders always return `Blob`. | Function | Description | |---|---| | `base64::encode(input) -> String` | Standard alphabet, padded. `input` is `String` or `Blob`. | | `base64::decode(s) -> Blob` | Decode standard alphabet. Throws on invalid. | | `base64::encode_url(input) -> String` | URL-safe alphabet, **no padding**. | | `base64::decode_url(s) -> Blob` | Decode URL-safe alphabet. Throws on invalid. | ```rhai let token = base64::encode_url(random::bytes(32)); // URL-safe session token let raw = base64::decode("aGVsbG8="); ``` --- ## `hex::` — hexadecimal Encode produces lowercase. Decode accepts mixed case. | Function | Description | |---|---| | `hex::encode(input) -> String` | Lowercase hex. `input` is `String` or `Blob`. | | `hex::decode(s) -> Blob` | Decode hex (case-insensitive). Throws on invalid. | ```rhai let fingerprint = hex::encode(random::bytes(20)); ``` --- ## `url::` — percent-encoding Unreserved set per RFC 3986 (`A-Z`, `a-z`, `0-9`, `-`, `_`, `.`, `~`) is preserved; everything else is percent-encoded. | Function | Description | |---|---| | `url::encode(s) -> String` | Percent-encode a component value. | | `url::decode(s) -> String` | Percent-decode. Throws on invalid UTF-8 in the decoded output. | | `url::encode_query(map) -> String` | Build `k1=v1&k2=v2` from a Map. Both keys and values are percent-encoded. Non-string values are coerced via `to_string()`. | `url::encode_query` emits keys in the Map's natural order, which is alphabetical (Rhai's `Map` is a `BTreeMap`). RFC 3986 leaves query parameter ordering unspecified, so this is fine for any conforming consumer; if you need a specific ordering, build the string by hand. ```rhai let qs = url::encode_query(#{ q: "rust regex", page: 2 }); // → "page=2&q=rust%20regex" ``` --- ## Request context (`ctx`) and the response envelope Every script sees a `ctx` map describing the invocation. The request fields under `ctx.request` are: | Field | Type | Notes | |---|---|---| | `ctx.request.path` | `String` | Request path. | | `ctx.request.method` | `String` | Uppercased HTTP verb (`"GET"`, `"POST"`, …). Empty for non-HTTP triggers. Branch on it to serve several verbs from one script. | | `ctx.request.headers` | `Map` | Lowercased header names → values. | | `ctx.request.body` | parsed | Already-parsed JSON when `Content-Type: application/json`; otherwise a string. | | `ctx.request.params` | `Map` | Captures from `:name` route segments. | | `ctx.request.query` | `Map` | Query-string parameters. | | `ctx.request.rest` | `String` | Suffix captured by a `prefix` (`/*`) route. | ```rhai // One script, many verbs: let m = ctx.request.method; if m == "GET" { /* list */ } else if m == "POST" { /* create */ } else { return #{ statusCode: 405, body: #{ error: "method not allowed" } }; } ``` ### The response envelope is `statusCode`-gated A returned map is unwrapped into `{statusCode, headers, body}` **only when it contains a `statusCode` key**. Without `statusCode`, the *entire* returned map becomes the response body, with an implicit `200`: ```rhai // WRONG — no statusCode, so the whole map is the body: #{ body: #{ token: t } } // → HTTP 200 {"body":{"token":"…"}} (double-nested!) // RIGHT — statusCode present, so it's treated as the envelope: #{ statusCode: 200, body: #{ token: t } } // → HTTP 200 {"token":"…"} ``` Always include `statusCode` when you mean to set headers or a specific body shape. Returning a bare value (`42`, `#{ ok: true }`) is fine — it becomes a `200` body as-is. **Abort with a status by *returning* the envelope, never `throw`.** A `throw` (even `throw #{ statusCode: 403 }`) is an uncaught runtime error → HTTP 502, not your status. Return a `statusCode` envelope to short-circuit: ```rhai if !authorized { return #{ statusCode: 403, body: #{ error: "forbidden" } }; } ``` ### Binary responses — `body_base64` To return raw bytes (an image, a PDF, a download) instead of JSON, put the base64 of the bytes in a `body_base64` field and set `Content-Type` yourself. `body` is ignored when `body_base64` is present. ```rhai #{ statusCode: 200, headers: #{ "content-type": "image/png" }, body_base64: file.data } // base64 string → raw PNG bytes on the wire ``` Absent a `Content-Type` the platform defaults to `application/octet-stream`. ### Non-JSON request bodies `ctx.request.body` is the parsed JSON for a JSON request. A non-JSON body is no longer rejected: `application/x-www-form-urlencoded` arrives as an object of string fields, `text/*` as the raw string, and any other content type as a base64 string of the raw bytes. Read `ctx.request.content_type` (a convenience mirror of the `content-type` header) to decide how to interpret `ctx.request.body`. ### Security note — `users::email_available` and account enumeration `users::email_available(email) -> bool` is the anonymous-safe way to pre-check an email during self-serve registration (`users::find_by_email` is forbidden for anonymous public scripts to prevent enumeration). The probe returns only `true`/`false` — the same bit a register form already leaks via "email already taken" (and via `create`'s own uniqueness error), so it adds no *new* enumeration vector — but it is **cheap (no password hash), has no side effect, and is not rate limited**, which makes bulk probing faster than going through `create`. There is no built-in per-route throttle or CAPTCHA primitive today (only the global `PICLOUD_MAX_CONCURRENT_EXECUTIONS` cap). If account enumeration matters for your app, the available mitigation is a `kv`-based counter keyed on the client (e.g. an IP/email bucket you increment and check) — the same pattern you'd use for any custom rate limit: ```rhai // Pre-check, then create. Gate behind your own kv counter if enumeration // is a concern — the platform does not rate-limit this probe for you. if !users::email_available(email) { return #{ statusCode: 409, body: #{ error: "email already registered" } }; } ``` ## What's not here - **Crypto** (sha256/hmac/argon2/encryption) — deferred to a focused later PR. - **Timezones** — UTC only in v1.1.0. Format with an offset upstream if you need local time. - **JWT, YAML, XML, CSV, Markdown** — not planned for v1.1.x. - **Stateful services** (KV, docs, HTTP, cron, files, pubsub, secrets, email, users, queue, invoke) — land per the v1.1.x roadmap in the [blueprint §12](../serverless_cloud_blueprint.md).