diff --git a/CLAUDE.md b/CLAUDE.md index 9e27af9..1624147 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Authoritative design: [serverless_cloud_blueprint.md](serverless_cloud_blueprint.md). The blueprint is a living document — when architecture decisions are made in conversation that contradict it, treat the latest decision as truth and update the blueprint. -**Current focus (Phase 4, v1.1.0):** SDK foundation + stdlib utilities — the shape every v1.1.x service module hangs off, see [docs/sdk-shape.md](docs/sdk-shape.md). Subsequent v1.1.x releases (KV in v1.1.1, docs in v1.1.2, …) fill it in; see blueprint §12 for the full table. Phase 3 shipped end-to-end: admin auth, multi-app scoping, and Phase 3.5 capability gating (`manager-core::authz::{can, require, Capability}` + migration `0006_users_authz.sql`). Every v1.1+ table starts with `app_id UUID NOT NULL REFERENCES apps(id) ON DELETE CASCADE` and every Rhai SDK call resolves its app from the execution context. +**Current focus (Phase 4, v1.1.0):** SDK foundation + stdlib utilities — the shape every v1.1.x service module hangs off, see [docs/sdk-shape.md](docs/sdk-shape.md). Stdlib reference at [docs/stdlib-reference.md](docs/stdlib-reference.md). Subsequent v1.1.x releases (KV in v1.1.1, docs in v1.1.2, …) fill it in; see blueprint §12 for the full table. Phase 3 shipped end-to-end: admin auth, multi-app scoping, and Phase 3.5 capability gating (`manager-core::authz::{can, require, Capability}` + migration `0006_users_authz.sql`). Every v1.1+ table starts with `app_id UUID NOT NULL REFERENCES apps(id) ON DELETE CASCADE` and every Rhai SDK call resolves its app from the execution context. ## Three-Service Architecture diff --git a/docs/stdlib-reference.md b/docs/stdlib-reference.md new file mode 100644 index 0000000..ce371dc --- /dev/null +++ b/docs/stdlib-reference.md @@ -0,0 +1,215 @@ +# 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). + +**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" +``` + +--- + +## 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).