From 823747aa6bfb98be3ea47162c37d6a6da27e9007 Mon Sep 17 00:00:00 2001 From: Nick Ricketts Date: Sun, 12 Jul 2026 23:32:15 -0500 Subject: [PATCH] nerak repo --- README.md | 334 +++++++++++++++++++++++++++--------------------------- 1 file changed, 167 insertions(+), 167 deletions(-) diff --git a/README.md b/README.md index aa65c1d..0ee346a 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Nerak is a declarative framework for building asynchronous web applications in C Everything runs in Docker. No other local dependencies. -```bash +``` mkdir myapp && cd myapp wget https://docker.nightshadecoder.dev/nerak/compose.yml @@ -38,7 +38,7 @@ docker compose up Create `app.c` with the example below. Nerak watches for changes and hot-reloads on save. Use your own editor, or attach to the built-in TUI with `docker compose attach nerak` for an integrated editor, LSP, and console. -```c +``` #include config(app){ @@ -99,7 +99,7 @@ Each `resource(...)` declares a named URL endpoint; each verb pipeline is a list Both pages share a layout, so `home` doubles as the layout: it declares the nav and a `{{$body}}` block whose default is the welcome page. The `todos` page extends it with `{{< home}}...{{/home}}`, overriding that block. Any template that declares a `{{$block}}` can be a parent; there is no special layout type. **`home.html`** -```html +``` @@ -113,7 +113,7 @@ Both pages share a layout, so `home` doubles as the layout: it declares the nav ``` **`todos.html`** -```html +``` {{< home}} {{$body}}

My Todos

@@ -123,7 +123,7 @@ Both pages share a layout, so `home` doubles as the layout: it declares the nav ``` **`app.c`** -```c +``` #include config(app){ @@ -151,7 +151,7 @@ Bring in SQLite with `#include `, declare a database with `sqlite_conf Three new SQL files: **`create_todos_table.sql`** -```sql +``` CREATE TABLE todos ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL @@ -159,12 +159,12 @@ CREATE TABLE todos ( ``` **`seed_todos.sql`** -```sql +``` INSERT INTO todos(title) VALUES('Learn Nerak'); ``` **`get_todos.sql`** -```sql +``` select id, title from todos; ``` @@ -223,7 +223,7 @@ Query parameters: database name, SQL asset, context key for the result table (`t Add a `.post` verb that validates, inserts, and redirects (POST-redirect-GET). A resource-scoped `.errors` handler re-renders the form on validation failure. **`create_todo.sql`** -```sql +``` insert into todos(title) values({{title}}); ``` @@ -301,7 +301,7 @@ A `/todos/:id` page fetches a todo and its comments concurrently, then nests the Three new SQL files and one new template: **`create_comments_table.sql`** -```sql +``` CREATE TABLE comments ( id INTEGER PRIMARY KEY AUTOINCREMENT, todo_id INTEGER NOT NULL REFERENCES todos(id), @@ -310,19 +310,19 @@ CREATE TABLE comments ( ``` **`get_todo.sql`** -```sql +``` select id, title from todos where id = {{id}}; ``` **`get_comments.sql`** -```sql +``` select id, todo_id, body from comments where todo_id = {{id}}; ``` Enter `{{#todo_data}}` first; after the join, `comments` lives inside each todo record: **`todo.html`** -```html +``` {{< home}} {{$body}} {{#todo_data}} @@ -469,7 +469,7 @@ A task is a named, reusable pipeline. Define it once with optional `.cron`; disp Two new SQL files: **`create_daily_stats_table.sql`** -```sql +``` CREATE TABLE daily_stats ( id INTEGER PRIMARY KEY AUTOINCREMENT, recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, @@ -478,7 +478,7 @@ CREATE TABLE daily_stats ( ``` **`record_daily_stats.sql`** -```sql +``` insert into daily_stats(todo_count) select count(*) from todos; ``` @@ -745,7 +745,7 @@ config(todos){ The `activity` module owns its own table, query, template, and subscriber. Nothing in it references the todos module: **`activity/create_activity_table.sql`** -```sql +``` CREATE TABLE activities ( id INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT NOT NULL, @@ -755,17 +755,17 @@ CREATE TABLE activities ( ``` **`activity/insert_activity.sql`** -```sql +``` insert into activities(kind, ref) values('created', {{title}}); ``` **`activity/get_activities.sql`** -```sql +``` select kind, ref, created_at from activities order by created_at desc; ``` **`activity/activity.html`** -```html +``` {{< home}} {{$body}}

Activity

@@ -779,7 +779,7 @@ select kind, ref, created_at from activities order by created_at desc; ``` **`activity/activity.c`** -```c +``` #include #include #include @@ -849,34 +849,34 @@ Base-spec features: Built-in helpers use `{{helper:args}}` syntax. Arguments are colon-separated, in order; each can be a literal or a context key. **`{{precision:field:N}}`**: format a numeric value with N decimal places. -```html +```

Total: ${{precision:total:2}}

``` **`{{input:field}}`**: raw, unvalidated request parameter from the `input` scope. Used to repopulate form fields after a validation error. -```html +``` ``` **`{{error:field}}`**: truthy when `field` has an error. Used as a Mustache section to conditionally render markup. -```html +``` {{#error:title}} invalid {{/error:title}} ``` **`{{error_message:field}}`**: human-readable message for a field error, from `input()`'s message or from `err_set()`. -```html +``` {{error_message:title}} ``` **`{{error_code:field}}`**: HTTP status code associated with a field error (e.g. `400`, `404`). -```html +```

Code: {{error_code:title}}

``` **`{{url:name}}`**: resolve a resource name to its URL. `:params` in the URL pattern are read from the current scope by name. -```html +``` All {{#todos_data}} {{title}} @@ -887,17 +887,17 @@ Built-in helpers use `{{helper:args}}` syntax. Arguments are colon-separated, in ``` **`{{asset:filename}}`**: resolve a file in `public/` to a cache-busted URL (content checksum + immutable cache headers). See [Static Files](#static-files). -```html +``` ``` **`{{csrf:param}}`**: emit a CSRF token for URL query strings. Generates a random hash, sets it on an httponly/secure/samesite cookie, outputs `csrf=` inline. -```html +``` Log out ``` **`{{csrf:input}}`**: emit a hidden `` carrying a CSRF token, for `
` use. Same cookie behavior as `{{csrf:param}}`. -```html +``` {{csrf:input}} @@ -906,12 +906,12 @@ Built-in helpers use `{{helper:args}}` syntax. Arguments are colon-separated, in ``` **`{{http_verb:param}}`**: emit an `http_method` override for URL query strings, letting a link reach a non-GET verb. One per verb: `{{http_get:param}}`, `{{http_post:param}}`, `{{http_put:param}}`, `{{http_patch:param}}`, `{{http_delete:param}}`, `{{http_sse:param}}`; each outputs `http_method=`. See [Resource Pipelines](#resource-pipelines). -```html +``` Delete ``` **`{{http_verb:input}}`**: emit a hidden `` carrying the `http_method` override, letting a `` (GET/POST only) reach any verb. One per verb: `{{http_get:input}}`, `{{http_post:input}}`, `{{http_put:input}}`, `{{http_patch:input}}`, `{{http_delete:input}}`, `{{http_sse:input}}`; each outputs ``. -```html +``` {{csrf:input}} {{http_delete:input}} @@ -930,7 +930,7 @@ An asset's name is the filename's basename (the part before the first dot). `get `mustache()`, `mdm()`, and the engine `*_query()` steps read a string from context by key and interpret it as a template or SQL. The step interprets whatever is under the key when it runs. `context(name, value)` does the same seeding from a string instead of a file. Useful for content too small to warrant its own file: -```c +``` context("hello", "

Hello, world!

"); // then mustache("hello", "hello_s") context("ping", "select 1"); // then sqlite_query({"db", "ping"}) ``` @@ -959,27 +959,27 @@ Each database engine is a module: `#include` its header (e.g. `#include sqlite_config( @@ -1001,17 +1001,17 @@ Nerak is resource-based, not route-based. Each `resource(...)` defines a named U Clients select a verb via the request method, or by passing `http_method` as a query/form parameter. This lets HTML forms (limited to GET/POST) reach any verb, and gives SSE a connection path: `/todos?http_method=sse`. Templates emit it via `{{http_verb:input}}` / `{{http_verb:param}}` (see [Templates](#templates)). **Resource name *(by order)***: identifier used by `{{url:name}}`, `redirect()`, and `reroute()`. -```c +``` resource("todos", "/todos", .get = { ... }); ``` **URL pattern *(by order)***: URL pattern. Supports `:params`. -```c +``` resource("todo", "/todos/:id", .get = { ... }); ``` **`.all`**: shared steps that run before every verb pipeline on the resource. -```c +``` resource("todo", "/todos/:id", .all = { input({"id", n_int, "must be a number"}) }, .get = { ... }, @@ -1020,12 +1020,12 @@ resource("todo", "/todos/:id", ``` **`.mime`**: default response content type. Values: `n_html`, `n_txt`, `n_es`, `n_json`, `n_js` (default `n_html`). -```c +``` resource("feed", "/feed.json", .mime = n_json, .get = { ... }); ``` **`.get` `.post` `.put` `.patch` `.delete`**: verb pipelines: ordered arrays of steps that transform a request into a response. -```c +``` resource("todos", "/todos", .get = { sqlite_query({"db", "get_todos", "todos_data"}), @@ -1040,7 +1040,7 @@ resource("todos", "/todos", ``` **`.sse`**: persistent SSE channel. The first value is the channel name (supports `{{interpolation}}`); any remaining steps run on connect. -```c +``` resource("todos", "/todos", .sse = {"todos:{{user_id}}", sqlite_query({"db", "get_todos", "todos_data"}), @@ -1050,7 +1050,7 @@ resource("todos", "/todos", ``` **`.errors` / `.repairs`**: resource-scoped error and repair pipelines. See [Error and Repair Pipelines](#error-and-repair-pipelines). -```c +``` resource("todos", "/todos", .post = { ... }, .errors = {{n_bad_request, { @@ -1061,7 +1061,7 @@ resource("todos", "/todos", ``` Combined: -```c +``` resource("todo", "/todos/:id", .all = {input({"id", n_positive, "must be a number"})}, .get = { @@ -1097,7 +1097,7 @@ Errors are terminal: the handler sends a response and ends the request. Repairs The `error` scope is shared across `input()` failures and `err_set()` calls: `{{error:name}}`, `{{error_code:name}}`, `{{error_message:name}}`. The raw input value remains in `input:name` for re-rendering forms. **Resource-scoped (`.errors` / `.repairs` fields):** -```c +``` resource("todos", "/todos", .post = { ... }, .errors = { @@ -1117,7 +1117,7 @@ resource("todos", "/todos", ``` **Module-scoped (`error()` / `repair()` calls):** -```c +``` config(todos){ error(n_error, { mustache("5xx", "error_s"), @@ -1143,33 +1143,33 @@ Internal pub/sub for cross-module communication. The publisher does not know who Events are durable. When a publisher is declared, Nerak creates a `nerak_events` database to track delivery. If the process crashes, undelivered events replay on the next boot. **`publish(event, .with = {...})`**: declares an outbound event contract. First value is the event name; `.with` lists context keys to pass along. -```c +``` publish("todo_created", .with = {"user_id", "title"} ); ``` **`subscribe(event, { steps })`**: registers a subscriber pipeline keyed by event name. -```c +``` subscribe("todo_created", { sqlite_query({"activity_db", "insert_activity"}) }); ``` **`emit(event)`**: a pipeline step that fires the event (see [emit](#emit)). -```c +``` emit("todo_created") ``` **`.errors` / `.repairs`** *(per subscriber)*: each `subscribe(...)` can declare its own handlers, resolved the same way as resource pipelines (the subscriber's own handlers, then its module's). See [Error and Repair Pipelines](#error-and-repair-pipelines). -```c +``` subscribe("todo_created", { sqlite_query({"activity_db", "insert_activity"}) }, .errors = {{n_error, {run(.call = log_subscriber_failure)}}}); ``` Combined: -```c +``` // todos/todos.c: publisher config(todos){ publish("todo_created", @@ -1211,14 +1211,14 @@ Dispatched tasks are durable: the dispatch module creates the persistent task ta Any pipeline or task can call `run()`, `run_worker()`, `run_task()`, and `dispatch()` (the last requires `dispatch.h`). **Task name *(by order)***: task identifier, invoked via `run_task("name")` or `dispatch("name")`. -```c +``` task("recount", { sqlite_query({"db", "recount_todos"}) }); ``` **Pipeline *(by order)***: the task's pipeline body, a brace block. -```c +``` task("name", { sqlite_query({...}), emit("done"), @@ -1227,28 +1227,28 @@ task("name", { ``` **`.accepts`**: context keys to pull from the caller into the task. -```c +``` task("recount_todos", { sqlite_query({"db", "recount"}) }, .accepts = {"user_id"}); ``` **`.cron`**: standard cron schedule for recurring tasks (no caller required). -```c +``` task("daily_digest", { sqlite_query({"db", "digest"}) }, .cron = "0 8 * * *"); ``` **`.errors` / `.repairs`** *(per task)*: each task can declare its own handlers, resolved the same way as resource pipelines (the task's own handlers, then its module's). See [Error and Repair Pipelines](#error-and-repair-pipelines). -```c +``` task("send_invoice", { fetch({.url = "https://api.billing.dev/invoices/{{invoice_id}}", .ctx_key = "inv"}) }, .repairs = {{n_not_authorized, {run(.call = refresh_billing_token)}}}); ``` Combined: -```c +``` // on-demand: dispatched via dispatch("recount_todos") task("recount_todos", { sqlite_query({"todos_db", "recount"}) @@ -1288,32 +1288,32 @@ Checks request parameters (query string, form body, URL params) against regex pa Built-in regex macros are defined in `nerak.h`; define your own the same way: `#define n_zipcode "^\\d{5}$"`. **`.ctx_key` *(by order)***: name of the parameter to validate. -```c +``` input({"title", "^\\S+$", "required"}) ``` **`.regex` *(by order)***: regex pattern, or a built-in validator macro. -```c +``` input({"email", n_email, "bad email"}) ``` **`.err_msg` *(by order)***: human-readable error shown via `{{error_message:name}}`. -```c +``` input({"age", n_int, "must be a number"}) ``` **`.opt`**: skip validation when the parameter is absent. -```c +``` input({"filter", "^(active|done)$", .opt = true}) ``` **`.def`**: default value injected when the parameter is absent. -```c +``` input({"page", n_int, .def = "1"}) ``` Combined: -```c +``` input( {"email", n_email, "must be a valid email"}, {"title", n_not_empty, "cannot be empty"}, @@ -1324,7 +1324,7 @@ input( ``` For checks beyond regex (uniqueness, cross-field rules, lookups), pair `input()` with a query and `run()`: -```c +``` input({"username", n_user, "must be alphanumeric"}), sqlite_query({"users_db", "find_username", "existing"}), run(^(){ @@ -1349,28 +1349,28 @@ run(^(){ Each engine provides its own query step: `sqlite_query()`, `postgres_query()`, `mysql_query()`, `redis_query()`, `duckdb_query()`. All share the same `query_c` shape. By order: first value is the database name (the `.nm` it was registered with), second is the context key holding the SQL, third is the `.ctx_key` for the result table (even single-row results are tables). Multiple items in one step run **concurrently**. Queries use prepared statements; interpolated `{{values}}` are bound, not spliced. For transactions, put `BEGIN`/`COMMIT`/`ROLLBACK` in the SQL. **`.db` *(by order)***: database name, matching the name a `_config(...)` was registered with. -```c +``` sqlite_query({"todos_db", "get_todos", "todos_data"}) ``` **`.q` *(by order)***: context key holding the SQL to run. -```c +``` sqlite_query({"todos_db", "get_todos", "todos_data"}) ``` **`.ctx_key` *(by order)***: context key for the result table. Optional; omit when the result isn't needed (e.g. an insert without `RETURNING`). -```c +``` sqlite_query({"todos_db", "create_todo"}) // no result captured sqlite_query({"todos_db", "get_todos", "todos_data"}) // result under "todos_data" ``` **`.err_on_empty`**: when true, raise `404 Not Found` if the query affects/returns zero rows. Default false. -```c +``` sqlite_query({"todos_db", "get_todo", "todo", .err_on_empty = true}) ``` **`.if_ctx` / `.not_ctx`** *(per item)*: conditionally include or skip individual queries while running the others concurrently. -```c +``` sqlite_query( {"db", "get_todos", "todos_data"}, {"db", "get_urgent", "urgent", .if_ctx = "show_urgent"} @@ -1378,7 +1378,7 @@ sqlite_query( ``` Combined: -```c +``` sqlite_query( {"todos_db", "get_todos", "todos_data"}, {"todos_db", "get_todo", "todo", .err_on_empty = true}, @@ -1401,19 +1401,19 @@ By order: `join(parent_key, parent_field, child_key, child_field)`. **child_field *(by order)***: field on the inner table that points back at the outer. **`.parent_join_key`**: name of the new field on outer records holding the matched inner records (defaults to the child table name). -```c +``` .parent_join_key = "todos" ``` Combined: -```c +``` join("projects", "id", "todos", "project_id") ``` **Full context example.** Concurrent query → `join()` → `mustache()`: fetch parent and children from separate queries, render as one nested structure. Blog + comments, single database: **`blog.html`** -```html +```
{{#blog}}

{{title}}

@@ -1428,7 +1428,7 @@ join("projects", "id", "todos", "project_id")
``` -```c +``` resource("blog", "/blogs/:id", .get = { input({"id", n_int}), @@ -1466,27 +1466,27 @@ Makes one or more HTTP requests and stores responses in context. JSON parses int Fields are given by name. `.url` and `.ctx_key` are the common pair; the rest are optional. **`.url`**: request URL; supports `{{interpolation}}`. -```c +``` fetch({.url = "https://api.weather.dev/forecast?city={{city}}", .ctx_key = "w"}) ``` **`.ctx_key`**: context key for the response. -```c +``` fetch({.url = "https://api.weather.dev/now", .ctx_key = "weather"}) ``` **`.meth`**: HTTP method. Defaults to `n_get`. Values: `n_get`, `n_post`, `n_put`, `n_patch`, `n_delete`, `n_sse`. -```c +``` fetch({.url = "https://api.dev/charge", .ctx_key = "r", .meth = n_post}) ``` **`.headers`**: array of name/value pairs. -```c +``` fetch({.url = "https://api.dev/me", .ctx_key = "r", .headers = {{"Authorization", "Bearer {{token}}"}}}) ``` **`.json_ctx_key`**: context key whose value is serialized as the JSON request body. -```c +``` fetch({.url = "https://api.dev/charge", .ctx_key = "receipt", .meth = n_post, .json_ctx_key = "order"}) ``` @@ -1496,12 +1496,12 @@ fetch({.url = "https://api.push.dev/notify", .meth = n_post, .json = "{\"text\": ``` **`.txt`**: context key sent as the plain-text request body. -```c +``` fetch({.url = "https://api.dev/log", .ctx_key = "r", .meth = n_post, .txt = "raw_body"}) ``` **`.if_ctx` / `.not_ctx`** *(per item)*: conditionally include or skip individual requests while running others concurrently. -```c +``` fetch( {.url = "https://api.weather.dev/now", .ctx_key = "weather"}, {.url = "https://api.quotes.dev/random", .ctx_key = "quote", .if_ctx = "show_quote"} @@ -1509,7 +1509,7 @@ fetch( ``` Combined, single request: -```c +``` fetch({ .url = "https://api.payments.dev/charge", .ctx_key = "receipt", @@ -1523,7 +1523,7 @@ fetch({ ``` Combined, concurrent fan-out: -```c +``` fetch( {.url = "https://api.weather.dev/now?city={{city}}", .ctx_key = "weather"}, {.url = "https://api.news.dev/headlines?topic={{topic}}", .ctx_key = "news"}, @@ -1536,7 +1536,7 @@ fetch( `run()` calls a C function or block inline on the reactor, with access to context via the [Imperative API](#imperative-api). It is where business logic and data shaping lives: enriching query results, aggregating, transforming data between steps, setting flags for [conditional](#conditionals) downstream steps. For short, non-blocking work; call `err_set()` to trigger an error/repair pipeline. Use `run_worker()` instead when the body would stall the reactor. **Block *(by order)***: inline block, for short logic specific to this pipeline. Here, attaching each challenger's opponent id so the template can render two voting forms with the right winner/loser pairing: -```c +``` run(^(){ auto const t = get("challengers"); auto const p0 = tbl_get(t, 0); @@ -1547,7 +1547,7 @@ run(^(){ ``` **`.call`**: reference to a named C function, for logic reuse across pipelines. -```c +``` run(.call = assign_opponents) ``` @@ -1558,7 +1558,7 @@ Inside blocks and `.call` functions, context, memory, errors, tables, and record `run_worker()` takes the same block or `.call` as `run()` but is for blocking or CPU-bound work: external C libraries, blocking I/O, heavy computation. The work is dispatched to the shared thread pool, releasing the reactor; the pipeline resumes on the original reactor when the call returns. Use it when the body would stall a request reactor. **Block *(by order)***: inline block, run on the shared thread pool. Here, rendering Markdown through an external C library and freeing its buffer when the request completes: -```c +``` run_worker(^(){ auto const raw = third_party_render_md(get("markdown")); defer_free(raw); @@ -1567,7 +1567,7 @@ run_worker(^(){ ``` **`.call`**: reference to a named C function, run on the shared thread pool. -```c +``` run_worker(.call = resize_image) ``` @@ -1576,7 +1576,7 @@ run_worker(.call = resize_image) Triggers an internal pub/sub event. Subscribers in other modules react in their `subscribe()` pipelines, with no direct dependency on the emitter. See [Event Pipelines](#event-pipelines). **Event name *(by order)***: name of the event to publish. -```c +``` emit("todo_created") ``` @@ -1585,7 +1585,7 @@ emit("todo_created") Runs a named task inline as a step in the calling pipeline; control returns to the next step when it finishes. For reusable pipelines composed into workflows. The task must be defined with `task(name, { ... })`. See [Task Pipelines](#task-pipelines). **Task name *(by order)***: name of a defined task. -```c +``` run_task("recount_todos") ``` @@ -1594,7 +1594,7 @@ run_task("recount_todos") Enqueues a named task as a durable background job; the calling pipeline continues immediately. Task reactors pick up queued jobs and execute their pipelines. The task is checkpointed after each step, so a crash mid-task resumes where it stopped. Requires `#include `, which provides the persistent task tables. The task must be defined with `task(name, { ... })`. See [Task Pipelines](#task-pipelines). **Task name *(by order)***: name of a defined task. -```c +``` dispatch("record_daily_stats") ``` @@ -1603,27 +1603,27 @@ dispatch("record_daily_stats") Pushes a Server-Sent Event. With `.chan`, the event broadcasts to all clients on that channel. Without it, the event returns to the requesting client. See [Resource Pipelines](#resource-pipelines). **`.chan` *(by order)***: channel to broadcast on; supports `{{interpolation}}`. -```c +``` sse("todos:{{user_id}}", .evt = "new_todo", .d = {"{{todo}}"}) ``` **`.evt`**: SSE `event:` line value. -```c +``` sse(.evt = "ping") ``` **`.d`**: array of strings, one per SSE `data:` line (multi-line data). -```c +``` sse(.evt = "msg", .d = {"line one", "line two"}) ``` **`.cmt`**: SSE `:` comment line value, useful for keep-alives. -```c +``` sse(.cmt = "keep-alive") ``` Combined: -```c +``` sse("todos:{{user_id}}", .evt = "todo_updated", .d = {"id: {{todo_id}}", "title: {{title}}"}, @@ -1636,22 +1636,22 @@ sse("todos:{{user_id}}", Renders a template into the pipeline context. `mustache()` renders Mustache; `mdm()` renders Markdown-with-Mustache; `json()` renders JSON. All take the same `render_c`. **`.template_ctx_key` *(by order)***: context key holding the template string to render. -```c +``` mustache("todos", "todos_s") ``` **`.ctx_key` *(by order)***: context key to write the rendered output to. -```c +``` mustache("todos", "todos_s") ``` JSON: -```c +``` json("todos", "todos_j") ``` Markdown-with-Mustache: -```c +``` context("welcome", "# Welcome, {{user_name}}"); mdm("welcome", "welcome_s") ``` @@ -1661,22 +1661,22 @@ mdm("welcome", "welcome_s") Sends a pipeline context value as the HTTP response. **`.ctx_key` *(by order)***: key of the rendered content to send. -```c +``` respond("todos_s") ``` **`.status`**: HTTP response status (default `n_ok`). Values: `n_ok` (200), `n_created` (201), `n_redirect` (302), `n_bad_request` (400), `n_not_authorized` (401), `n_not_found` (404), `n_error` (500). -```c +``` respond("not_found_s", .status = n_not_found) ``` **`.mime`**: override the response content type. Values: `n_html`, `n_txt`, `n_es`, `n_json`, `n_js`. -```c +``` respond("plain_s", .mime = n_txt) ``` Combined: -```c +``` mustache("not_found", "not_found_s"), respond("not_found_s", .status = n_not_found) ``` @@ -1686,15 +1686,15 @@ respond("not_found_s", .status = n_not_found) Set HTTP response headers and cookies declaratively. Both accept an array of name/value pairs; values support `{{interpolation}}`. **Pairs *(by order)***: array of `{name, value}` entries. -```c +``` headers({{"X-Request-Id", "{{request_id}}"}}) ``` -```c +``` cookies({{"session", "{{session_id}}"}}) ``` Combined: -```c +``` headers({ {"X-Request-Id", "{{request_id}}"}, {"Cache-Control", "no-store"} @@ -1710,7 +1710,7 @@ cookies({ `redirect()` returns a 302 to the client, causing the browser to navigate. `reroute()` re-enters the router server-side, executing another resource's pipeline within the same request. Both take only the target resource name. `:params` in the target's URL pattern are read from the current context by matching key names. **Resource name *(by order)***: target resource name. Required `:params` are read from context by name. -```c +``` redirect("todos") // 302 to /todos redirect("todo") // 302 to /todos/{{id}}, id read from context redirect("org_todo") // 302 to /orgs/{{org}}/todos/{{id}}, org and id read from context @@ -1722,12 +1722,12 @@ reroute("todo") // run that pipeline in-process, id read from context Groups multiple steps into a single composite step. Useful when applying one `.if_ctx`/`.not_ctx` to several steps without repeating it. **`.steps` *(by order)***: array of steps that run as a unit. -```c +``` nest({sqlite_query({...}), emit("urgent_todo"), mustache("urgent", "urgent_s"), respond("urgent_s")}) ``` **`.if_ctx` / `.not_ctx`**: condition applied to the whole group. -```c +``` nest({sqlite_query({...}), emit("urgent_todo"), mustache("urgent", "urgent_s"), respond("urgent_s")}, .if_ctx = "is_urgent") ``` @@ -1749,27 +1749,27 @@ Functions called from `run()`/`run_worker()` blocks and `.call` functions to rea Read, write, and test context keys, and resolve `{{interpolation}}` against the current scope. **`get(name)`**: returns the value stored under `name`, or `nullptr` if absent. The returned pointer is whatever was stored: a `string` for scalars, a `table` for query and fetch results. -```c +``` auto todos = get("todos"); ``` **`set(name, value)`**: writes `value` to `name`, exposing it to downstream steps and templates. -```c +``` set("is_urgent", "1"); ``` **`has(name)`**: returns true when `name` exists in the current scope. -```c +``` if (has("user_id")) { ... } ``` **`fmt(fmtstr)`**: returns `fmt` with `{{name}}` interpolations resolved against the current context. Same scopes and helpers as templates. -```c +``` auto greeting = fmt("Hello, {{user_name}}"); ``` Combined: -```c +``` run(^(){ auto rows = get("todos"); if (tbl_len(rows) > 5) { @@ -1784,18 +1784,18 @@ run(^(){ Pipeline-arena allocation and deferred cleanup of foreign pointers. Both clear when the request completes. **`alloc(sz)`**: returns a buffer from the pipeline arena. Reclaimed automatically on request completion. -```c +``` auto buf = alloc(256); ``` **`defer_free(ptr)`**: schedules `free()` for a pointer returned by an external library. Runs when the arena is released. -```c +``` auto out = third_party_alloc(256); defer_free(out); ``` Combined: -```c +``` run_worker(^(){ auto url = alloc(512); build_signed_url(url, 512, get("path")); @@ -1812,22 +1812,22 @@ run_worker(^(){ Raise field-scoped errors from `run()` to trigger error/repair pipelines. Keys land in the `error:name` scope, visible to templates as `{{error:name}}`, `{{error_code:name}}`, and `{{error_message:name}}`. **`err_set(name, err)`**: associates an error with `name` and triggers the nearest [error or repair pipeline](#error-and-repair-pipelines). -```c +``` err_set("token", (err){ n_bad_request, "token has expired" }); ``` **`err_get(name)`**: returns the `err` previously set on `name`. -```c +``` auto e = err_get("token"); ``` **`err_has(name)`**: returns true when `name` has an error. -```c +``` if (err_has("token")) { ... } ``` Combined: -```c +``` run(^(){ auto token = get("token"); if (!token || strlen(token) < 16) { @@ -1844,37 +1844,37 @@ run(^(){ Tables are ordered collections of records, the shape `query()` produces and `fetch()` parses JSON into. Use these to build derived results. **`tbl_new()`**: returns an empty table in the pipeline arena. -```c +``` auto t = tbl_new(); ``` **`tbl_len(t)`**: number of records in `t`. -```c +``` auto n = tbl_len(get("todos")); ``` **`tbl_get(t, i)`**: record at index `i`, or `nullptr` if out of range. -```c +``` auto first = tbl_get(get("todos"), 0); ``` **`tbl_add(t, r)`**: appends `r` to `t`. -```c +``` tbl_add(t, rec_new()); ``` **`tbl_rem(t, r)`**: removes record `r` from `t`. -```c +``` tbl_rem(t, r); ``` **`tbl_rem_at(t, i)`**: removes the record at index `i`. -```c +``` tbl_rem_at(t, 0); ``` Combined: -```c +``` run(^(){ auto source = get("raw_users"); auto active = tbl_new(); @@ -1894,27 +1894,27 @@ run(^(){ Records are name-value bags, the shape of one row from `query()` or one object from `fetch()`. All values are strings; see [Everything is a String](#everything-is-a-string). **`rec_new()`**: returns an empty record in the pipeline arena. -```c +``` auto r = rec_new(); ``` **`rec_get(r, name)`**: string value of `name`, or `nullptr` if absent. -```c +``` auto title = rec_get(r, "title"); ``` **`rec_set(r, name, value)`**: writes `value` to `name` on `r`. -```c +``` rec_set(r, "title", "New title"); ``` **`rec_rem(r, name)`**: removes `name` from `r`. -```c +``` rec_rem(r, "draft"); ``` Combined: -```c +``` run(^(){ auto todos = get("todos"); for (int i = 0; i < tbl_len(todos); i++) { @@ -1932,18 +1932,18 @@ run(^(){ Every step accepts `.if_ctx` and `.not_ctx`, naming a context variable. They work for any context value: validated inputs, query results, framework flags like `is_htmx`, or flags set from `run()`. **`.if_ctx`**: context key. Step runs only when the value is present. -```c +``` mustache("fragment", "frag_s", .if_ctx = "is_htmx") ``` **`.not_ctx`**: context key. Step runs only when the value is absent. -```c +``` mustache("full_page", "page_s", .not_ctx = "is_htmx") ``` For multi-state branching, set context flags from `run()`, then key downstream steps off them: -```c +``` run(.call = classify_todo), mustache("urgent_confirmation", "urgent_s", .if_ctx = "is_urgent"), respond("urgent_s", .if_ctx = "is_urgent"), @@ -1956,14 +1956,14 @@ respond("standard_s", .not_ctx = "is_urgent") `.map` and `.map_key` run a step once per row of a context table, all rows **concurrently**, like multiple items in `query()` or `fetch()`. With a `.ctx_key`, results are collected into a table aligned with the input, one entry per row. They differ in how each row reaches the step body: `.map` puts the row's fields in scope as bare `{{interpolations}}`; `.map_key` binds the row as a single-row table under a named key. **`.map`**: name of a context table to iterate over. The row's fields land in scope as bare interpolations. -```c +``` // One request per row in `users`, all concurrent. // Each row's `id` fills the URL; responses collected into `profiles`, aligned with `users`. fetch({.url = "https://api.users.dev/{{id}}", .ctx_key = "profiles", .map = "users"}) ``` **`.map_key`**: context key under which the current row is exposed as a single-row table. Pairs with `.map`. -```c +``` // Render the `todo` template once per row of `todos`. // `.map_key = "todo_d"` presents the current row as the single-row table `todo_d` // Rendered fragments are collected into `todo_s`, aligned with `todos`. @@ -1977,7 +1977,7 @@ A module is declared with `config(name)` in a `name.c` file, usually inside a ma A module is seeded with the assets in its folder and every asset up to the project root, at startup. See [Assets](#assets) and [Context](#context). **`config(name)`**: declares a module. -```c +``` // todos/todos.c config(todos){ // resources, databases, tasks, subscribers ... @@ -1985,7 +1985,7 @@ config(todos){ ``` **`middleware(steps)`**: registers shared steps that run on every request to a resource in the same module. Cross-cutting setup like session loading or tenant resolution lives here. -```c +``` config(todos){ middleware(session()); /* resources, ... */ } ``` @@ -1993,7 +1993,7 @@ config(todos){ middleware(session()); /* resources, ... */ } **Pipeline composition.** A request runs the resource's `.all` steps first, then the module's `middleware()`, then the verb pipeline. -```c +``` // todos/todos.c: session loads, resources require login #include #include @@ -2029,7 +2029,7 @@ For `GET /todos/5` the executed order is: `input({"id", ...})` (resource `.all`) **Complete module file.** A `blogs/blogs.c`: -```c +``` #include #include @@ -2085,7 +2085,7 @@ Bundled modules. Activate each by `#include`ing its header. Activate with `#include `. Serves the htmx runtime as the `{{> htmx }}` partial, and sets the `is_htmx` context flag on requests carrying the `HX-Request` header. Pair the flag with `.if_ctx`/`.not_ctx` to return a fragment to htmx and a full page to a direct visit, or use `hx-boost` to upgrade ordinary links and forms into AJAX swaps. -```c +``` #include #include @@ -2103,7 +2103,7 @@ config(todos){ ``` Include the runtime once in the page ``: -```html +``` {{> htmx }} ... ``` @@ -2115,33 +2115,33 @@ Activate with `#include `. Serves the Datastar runtime as the `{{> d `datastar()` patches a rendered fragment into the page by target element. The first value is the channel (supports `{{interpolation}}`). **`.chan` *(by order)***: channel to push to. -```c +``` mustache("todo", "todo_s"), datastar("todos:{{user_id}}", .target = "todos", .mode = ds_append, .elements = "todo_s") ``` **`.target`**: target element to patch, given as an element id or CSS selector; supports `{{interpolation}}`. -```c +``` .target = "todo_{{id}}" ``` **`.mode`**: how the rendered fragment is applied to the target (a `datastar_m`). -```c +``` .mode = ds_replace ``` **`.elements`**: context key holding the rendered HTML fragment to patch in. Not required for `ds_remove`. -```c +``` .elements = "todo_row_s" ``` **`.signals`**: context key holding signal state to merge into the client store. -```c +``` .signals = "ui_state" ``` **`.js`**: JavaScript to execute on the client. -```c +``` .js = "window.scrollTo(0, document.body.scrollHeight)" ``` @@ -2149,7 +2149,7 @@ datastar("todos:{{user_id}}", .target = "todos", .mode = ds_append, .elements = Worked example: a create, an update, and a delete, each pushing a patch to every client on the channel. Queries use `RETURNING` so the changed row comes back for rendering. This mirrors the shipped `app.c`. -```c +``` resource("todos", "/todos", .all = {logged_in()}, // Each browser opens this channel and listens for patches. @@ -2184,7 +2184,7 @@ resource("todo", "/todos/:id", Datastar sets a context flag on requests it originates, usable with `.if_ctx`. Include the runtime once in the page ``: -```html +``` {{> datastar }} ``` @@ -2194,7 +2194,7 @@ Include the runtime once in the page ``: Activate with `#include `. Compiles Tailwind utility classes used across the project's templates and serves the stylesheet as the `{{> tailwind }}` partial. Use Tailwind classes directly in templates; no build step or config file required. -```html +``` {{> tailwind }}

Vote for which is roundest

@@ -2205,7 +2205,7 @@ Activate with `#include `. Compiles Tailwind utility classes used ac Activate with `#include `. Compiles DaisyUI classes used across the project's templates and serves the stylesheet as the `{{> daisyui }}` partial. Use DaisyUI classes directly in templates; no build step or config file required. -```html +``` {{> daisyui }} @@ -2217,17 +2217,17 @@ Activate with `#include `. Compiles DaisyUI classes used across the p Activate with `#include `. Cookie-based authentication as pipeline steps. `session()` loads the current `user` record into context from the session cookie; run it as `middleware()` in each module whose pipelines need to know who is signed in. `logged_in()` guards a resource, redirecting anonymous visitors to the login page. `login()`, `logout()`, and `signup()` perform the corresponding actions. The login page template is the asset named `login`. **`session()`**: loads the current user into context from the session cookie. Use as middleware. -```c +``` middleware(session()); ``` **`logged_in()`**: requires an authenticated session; redirects to login otherwise. Use in a resource `.all`. -```c +``` resource("todos", "/todos", .all = {logged_in()}, .get = { ... }); ``` **`login()` / `logout()` / `signup()`**: authentication actions for the corresponding verb pipelines. -```c +``` resource("login", "/login", .get = { mustache("login", "login_s"), @@ -2246,7 +2246,7 @@ resource("signup", "/signup", ``` Combined: load the session in the module whose resources it gates, then read user fields in that module's templates. -```c +``` // todos/todos.c #include #include @@ -2277,7 +2277,7 @@ config(app){ resource("logout", "/logout", .post = {logout()}); } ``` -```html +``` {{#user}} Hi, {{short_name}} @@ -2288,7 +2288,7 @@ config(app){ Each engine is its own module: `#include` its header, then use `_config(...)` to register and `_query({...})` as a pipeline step. They share `db_c` and `query_c` from [Databases](#databases) and [query](#query); only `.conn` is engine-specific. -```c +``` #include // sqlite_config("...", "file:app.db?mode=rwc", ...); sqlite_query({...}); #include // postgres_config("...", "postgres://...", ...); postgres_query({...}); #include // mysql_config("...", "mysql://...", ...); mysql_query({...}); @@ -2307,7 +2307,7 @@ public/ └── logo.svg ``` -```html +``` Logo @@ -2326,7 +2326,7 @@ vendor/ └── cmark.h ``` -```c +``` #include #include "vendor/cmark/cmark.h" @@ -2409,7 +2409,7 @@ Built-in TUI editor with HMR, LSP support, and integrated source control. ### Introspection `/app_info` is a built-in resource in dev builds. Query it like any other endpoint: -```bash +``` curl localhost:3000/app_info # view topology curl localhost:3000/app_info/resources # list all resources curl localhost:3000/app_info/pipelines # inspect pipelines @@ -2420,20 +2420,20 @@ Production builds omit it; see [Deployment](#deployment). ### Testing Built-in runners for unit and end-to-end testing; no external framework setup required. -```bash +``` unit_tests # fast, criterion-based tests e2e_tests # playwright-powered browser tests ``` ### Debugging Pipeline-aware commands. Halt on individual pipeline steps, step through execution, and inspect the full pipeline context including nested tables and records. -```bash +``` app_debug # interactive debugger in the TUI ``` ### Deployment Nerak deploys as a standard Docker container. It does not terminate TLS; production deployments place Nerak behind a reverse proxy or load balancer (Nginx, Caddy, AWS ALB) to handle HTTPS. -```bash +``` app_build # outputs a minimal production Docker image ```