Files
mach_examples/llms.txt

559 lines
17 KiB
Plaintext
Raw Normal View History

2025-07-24 12:46:01 -05:00
# MACH
> MACH (Modern Asynchronous C Hypermedia) is a declarative framework for building asynchronous web applications in C23. No build configuration, no malloc/free, no thread management — the framework handles compilation, hot reload, memory, concurrency, I/O, and observability automatically.
## Quick Start
Everything runs in Docker.
```bash
mkdir myapp && cd myapp
wget https://docker.nightshadecoder.dev/mach/compose.yml
docker compose up # dev server :3000, telemetry :4000
```
Minimal app (`main.c`):
```c
#include <mach.h>
mach(main){
context("hello", "<h1>Hello, world!</h1>");
resource("home", "/", .get = {respond("hello")});
}
```
`mach(main)` runs once at boot. `context()` registers a named template inline. `resource()` declares an endpoint.
---
## Core Philosophy
An application is a data transformation: input arrives, gets transformed, leaves as output. MACH wraps that path as **pipelines** — ordered lists of steps that turn a request into a response.
### Everything is a String
The web is text. The pipeline context stores and passes data as strings. Strings are interpolated into SQL, templates, and URLs with `{{context_key}}`.
### CLAD Principles
- **(C)omposable** — small, independent steps chain into feature pipelines.
- **(L)ocality of Behavior** — SQL, templates, and logic for a feature live together.
- **(A)utonomous** — modules own their schemas, migrations, seeds, routes, UI, and logic.
- **(D)omain Based** — each module owns one slice of the app.
---
## Context
Pipelines share a scoped key-value store for one request. Three scopes:
- `input:xxx` — raw request parameters
- `error:xxx` — validation/error data
- unprefixed — app scope (query results, validated inputs, computed values)
`input()` promotes values from `input:` to app scope.
---
## Assets
Every non-`.c` file is an asset embedded at compile time. The asset name is the filename basename before the first dot:
- `get_todos.sql` → asset key `get_todos`
- `todos.mustache.html` → asset key `todos`
- `home.md` → asset key `home`
Assets seed into the module whose folder they live in. `context(name, value)` does the same from a string.
---
## Templates
MACH uses Mustache (`.mustache.html`) and MDM — Markdown + Mustache (`.md`).
**Core syntax:**
- `{{name}}` — HTML-escaped interpolation
- `{{{name}}}` / `{{&name}}` — unescaped
- `{{#name}}...{{/name}}` — section (truthy / iterates arrays)
- `{{^name}}...{{/name}}` — inverted section
- `{{> partial}}` — inline another asset
- `{{< parent}}{{$block}}override{{/block}}{{/parent}}` — layout inheritance
**Built-in helpers:**
- `{{precision:field:N}}` — format number to N decimals
- `{{input:field}}` — raw request param (repopulate forms)
- `{{error:field}}` — truthy section when field has error
- `{{error_message:field}}` — human-readable error message
- `{{error_code:field}}` — HTTP status code of error
- `{{url:name}}` — resolve resource name to URL; `:params` filled from current scope
- `{{asset:filename}}` — cache-busted URL for a file in `public/`
- `{{csrf:token}}` — CSRF token value
- `{{csrf:input}}` — hidden `<input>` with CSRF token
---
## Resource Pipelines
Resources are named URL endpoints. HTTP verb pipelines are ordered arrays of steps.
```c
resource("todo", "/todos/:id",
.all = {input({"id", m_positive, "must be a number"})},
.get = {
sqlite_query({"todos_db", "get_todo", "todo", .must_exist = true}),
mustache("todo", "todo_s"),
respond("todo_s")
},
.patch = {
input({"title", m_not_empty, "required"}),
sqlite_query({"todos_db", "update_todo"}),
redirect("todo")
},
.delete = {
sqlite_query({"todos_db", "delete_todo"}),
redirect("todos")
},
.sse = {"todo:{{id}}", sse(.event = "ready")},
.errors = {{m_not_found, {
mustache("404", "not_found_s"),
respond("not_found_s")
}}}
);
```
**Fields:**
- `.all` — steps that run before every verb pipeline
- `.get` `.post` `.put` `.patch` `.delete` — verb pipelines
- `.sse` — persistent SSE channel; first value is channel name
- `.mime` — default response content type (`m_html`, `m_json`, `m_txt`, `m_sse`, `m_js`)
- `.errors` / `.repairs` — resource-scoped error and repair handlers
Clients can override the verb via `?http_method=patch` — allows HTML forms to reach any verb.
---
## Pipeline Steps
All steps accept `.if_context` and `.unless_context` for conditional execution, and `.table_key` for concurrent fan-out across table rows.
### input
Validates request parameters against regex. On success, promotes to app scope. On failure, fires nearest error/repair handler with all errors collected.
```c
input(
{"email", m_email, "must be a valid email"},
{"title", m_not_empty, "cannot be empty"},
{"page", m_integer, "must be a number", .fallback = "1"},
{"filter", "^(active|done)$", .optional = true}
)
```
**Built-in validators:** `m_not_empty` `m_alpha` `m_alphanumeric` `m_slug` `m_no_html` `m_integer` `m_positive` `m_float` `m_percentage` `m_email` `m_uuid` `m_username` `m_date` `m_time` `m_datetime` `m_url` `m_ipv4` `m_hex_color` `m_zipcode_us` `m_phone_e164` `m_cron` `m_token` `m_base64` `m_boolean` `m_yes_no` `m_on_off`
### query
Each engine has its own step: `sqlite_query()`, `postgres_query()`, `mysql_query()`, `redis_query()`, `duckdb_query()`. Multiple items in one call run **concurrently**. Values interpolated into SQL (`{{user_id}}`) are bound as prepared-statement parameters.
```c
sqlite_query(
{"todos_db", "get_todos", "todos_data"},
{"todos_db", "get_urgent", "urgent", .if_context = "show_urgent"},
{"todos_db", "get_todo", "todo", .must_exist = true}
)
```
**Fields:** `.db` (database name) · `.query` (context key with SQL) · `.set_key` (result table key) · `.must_exist` (404 on zero rows) · `.if_context` / `.unless_context`
### join
Nests records from one context table into each matching record of another (in-memory JOIN). Used when data comes from separate databases or queries.
```c
// after: comments lives inside each blog record
join("blog", "id", "comments", "blog_id")
```
### fetch
Makes HTTP requests and stores responses in context. JSON parses into tables; plain text stored as string. Multiple items run **concurrently**.
```c
fetch(
{"https://api.weather.dev/now?city={{city}}", "weather"},
{"https://api.news.dev/headlines", "news"},
{"https://api.quotes.dev/random", "quote", .if_context = "show_quote"}
)
```
**Fields:** `.url` · `.set_key` · `.method` (`m_get` `m_post` `m_put` `m_patch` `m_delete` `m_sse_method`) · `.headers` · `.json` (context key → JSON body) · `.text` (context key → plain body)
### exec and worker
`exec()` — runs a C block or function for business logic, data shaping, setting flags. Runs inline on the reactor.
`worker()` — same as `exec()` but dispatches to the shared thread pool for blocking/CPU-bound work.
```c
exec(^(){
auto rows = get("todos");
if (table_count(rows) > 5)
set("is_urgent", "1");
})
```
### emit
Fires an internal pub/sub event. Subscribers in other modules react; no direct coupling.
```c
emit("todo_created")
```
### run
Enqueues a named task; the calling pipeline continues immediately.
```c
run("record_daily_stats")
```
### sse
Pushes a Server-Sent Event. With `.channel`, broadcasts to all clients on that channel.
```c
sse("todos:{{user_id}}", .event = "todo_updated", .data = {"id: {{todo_id}}", "title: {{title}}"})
```
### render
`mustache(template_key, set_key)` — Mustache template
`mdm(template_key, set_key)` — Markdown + Mustache
`json(template_key, set_key)` — JSON serialization
### respond
Sends a context value as the HTTP response.
```c
respond("todos_s")
respond("not_found_s", .status = m_not_found)
respond("data_j", .mime = m_json)
```
**Status values:** `m_ok` (200) · `m_created` (201) · `m_redirect` (302) · `m_bad_request` (400) · `m_not_authorized` (401) · `m_not_found` (404) · `m_error` (500)
### headers and cookies
```c
headers({{"X-Request-Id", "{{request_id}}"}, {"Cache-Control", "no-store"}}),
cookies({{"session", "{{session_id}}"}})
```
### redirect and reroute
`redirect("todos")` — 302 to the named resource; `:params` read from context.
`reroute("todos")` — re-enters the router server-side, executes another resource's pipeline in-process.
### nest
Groups steps into a composite so one `.if_context` applies to all:
```c
nest({sqlite_query({...}), mustache("urgent", "s"), respond("s")}, .if_context = "is_urgent")
```
---
## Conditionals
Every step accepts `.if_context` (run only when key is present) and `.unless_context` (run only when key is absent). Set flags from `exec()`, key downstream steps off them.
```c
mustache("fragment", "frag_s", .if_context = "is_htmx"),
respond("frag_s", .if_context = "is_htmx"),
mustache("full_page", "page_s",.unless_context = "is_htmx"),
respond("page_s", .unless_context = "is_htmx")
```
---
## Iteration
`.table_key` names a context table; the step runs once per row **concurrently**, with that row's fields in scope.
```c
// One HTTP request per row in `users`, all concurrent; responses in `profiles`
fetch({"https://api.users.dev/{{id}}", "profiles", .table_key = "users"})
```
---
## Error and Repair Pipelines
When a step fails, MACH finds a handler matching the error code — checking resource-scoped handlers first, then module-scoped.
- **Errors** — terminal; handler sends response and ends the request.
- **Repairs** — resumable; fix context, then resume the pipeline after the failure point.
```c
// Resource-scoped
.errors = {{m_bad_request, {mustache("form", "form_s"), respond("form_s")}}}
.repairs = {{m_not_authorized, {exec(.call = refresh_session_token)}}}
// Module-scoped
error(m_not_found, {mustache("404", "not_found_s"), respond("not_found_s")});
repair(m_not_authorized, {exec(.call = refresh_token)});
```
**Built-in codes:** `m_bad_request` (400) · `m_not_authorized` (401) · `m_not_found` (404) · `m_error` (500). Any integer works; define custom codes with `#define`.
---
## Event Pipelines (Pub/Sub)
Cross-module communication with no direct dependency. Activate with `#include <pubsub.h>`.
Events are **durable**: undelivered events replay on next boot after a crash.
```c
// Publisher (todos/todos.c)
publish("todo_created", .with = {"user_id", "title"});
// ...
emit("todo_created") // pipeline step
// Subscriber (activity/activity.c)
subscribe("todo_created", {
sqlite_query({"activity_db", "insert_activity"})
});
```
---
## Task Pipelines
Named pipelines that run asynchronously on task reactors. Fire-and-forget; the calling pipeline continues immediately.
Tasks are **durable**: context is checkpointed after each step; a crash mid-task resumes on next boot.
```c
// On-demand
task("recount_todos", {
sqlite_query({"todos_db", "recount"})
}, .accepts = {"user_id"}); // pulls these keys from caller
// Recurring
task("daily_digest", {
sqlite_query({"todos_db", "digest"}),
emit("digest_ready")
}, .cron = "0 8 * * *");
// Enqueue from a pipeline
run("recount_todos")
```
---
## Modules and Composition
A module is a folder with a matching `<name>/<name>.c` file declaring `mach(name){ ... }`.
- Modules own their assets, databases, migrations, tasks, event contracts, and middleware.
- Compose by `#include`ing a module's `.c` file into `main.c`.
- `middleware(steps)` — shared steps that run on every request in the module.
- Request execution order: resource `.all` → module `middleware()` → verb pipeline.
```c
// main.c
#include <mach.h>
#include "todos/todos.c"
#include "activity/activity.c"
mach(main){
middleware(session());
resource("home", "/", .get = {mustache("home", "home_s"), respond("home_s")});
}
// todos/todos.c
mach(todos){
middleware(logged_in(), session());
sqlite_database(.name = "todos_db", .connect = "file:todos.db?mode=rwc",
.migrations = {"create_todos_table"}, .seeds = {"seed_todos"});
resource("todos", "/todos",
.get = {sqlite_query({"todos_db", "get_todos", "todos_data"}), mustache("todos", "todos_s"), respond("todos_s")},
.post = {input({"title", m_not_empty}), sqlite_query({"todos_db", "create_todo"}), redirect("todos")},
.errors = {{m_bad_request, {reroute("todos")}}}
);
}
```
---
## Databases
Activate an engine with `#include <sqlite.h>` (or `postgres.h`, `mysql.h`, `redis.h`, `duckdb.h`). Register with `<engine>_database(...)`. Query with `<engine>_query({...})`.
Migrations and seeds are forward-only and index-based; tracked in `mach_meta`. Multi-tenant databases use `{{interpolation}}` in `.connect`.
```c
sqlite_database(
.name = "todos_db",
.connect = "file:{{user_id}}_todo.db?mode=rwc",
.migrations = {"create_todos_table", "create_comments_table"},
.seeds = {"seed_todos"}
);
```
---
## Imperative API
Used inside `exec()` and `worker()` blocks for custom logic.
### Context
```c
get("key") // returns string or table, nullptr if absent
set("key", "value") // write to app scope
has("key") // bool presence check
format("Hello {{name}}") // interpolate against current context
```
### Memory
```c
allocate(256) // arena buffer, freed on request completion
defer_free(ptr) // schedule free() for external library pointer
```
### Errors
```c
error_set("field", (error){m_bad_request, "message"})
error_get("field") // returns error struct
error_has("field") // bool
```
### Tables
```c
table_new() // empty table in arena
table_count(t) // row count
table_get(t, i) // record at index i
table_add(t, r) // append record
table_remove(t, r) // remove record
table_remove_at(t, i) // remove at index
```
### Records
```c
record_new() // empty record in arena
record_get(r, "field") // string value or nullptr
record_set(r, "field", "value") // write field
record_remove(r, "field") // remove field
```
---
## Bundled Modules
### htmx (`#include <htmx.h>`)
Serves the htmx runtime as `{{> htmx }}`. Sets `is_htmx` context flag on htmx requests. Pair with `.if_context`/`.unless_context` to return fragments vs full pages.
### datastar (`#include <datastar.h>`)
Serves the Datastar runtime as `{{> datastar }}`. Provides `datastar_sse()` for pushing reactive fragment and signal patches over an SSE channel.
```c
mustache("todo_row", "todo_row_s"),
datastar_sse("todos:{{user_id}}",
.target = "#todo-list",
.mode = mode_append,
.elements = "todo_row_s"
)
```
Patch modes: `mode_outer` `mode_inner` `mode_replace` `mode_prepend` `mode_append` `mode_before` `mode_after` `mode_remove`
### tailwind (`#include <tailwind.h>`)
Compiles Tailwind classes used in templates. Serves stylesheet as `{{> tailwind }}`. No config or build step required.
### session_auth (`#include <session_auth.h>`)
Cookie-based authentication as pipeline steps.
```c
session() // middleware: loads user from session cookie into context
logged_in() // guard: redirects anonymous visitors to login
login() // action step
logout() // action step
signup() // action step
```
---
## Static Files
Files in `public/` are served directly. Reference with `{{asset:filename}}` for a content-checksummed, cache-busted URL with immutable cache headers.
---
## External Dependencies
Drop third-party C source into `vendor/`. MACH compiles and links them automatically. For non-source dependencies, provide a custom `Dockerfile`.
```c
#include "vendor/cmark/cmark.h"
worker(^(){
auto html = cmark_markdown_to_html(get("markdown"), strlen(get("markdown")), 0);
defer_free(html);
set("html_content", html);
})
```
---
## Architecture
### Boot-Time Compilation
`mach(main)` runs once at boot. Registration calls are processed into a precompiled execution graph. Each incoming request executes a pre-warmed pipeline sequence — no runtime parsing.
### Multi-Reactor Architecture
- **Request reactors** — one per dedicated CPU core; handles HTTP traffic.
- **Task reactors** — one per dedicated core; monitors task database, processes cron.
- **Shared thread pool** — remaining cores; handles `worker()` dispatches (blocking/CPU-bound I/O).
### Safe by Default
- **Memory** — arena allocators per request; no malloc/free in application code; bounds-checked data structures; 5MB pipeline memory limit (configurable).
- **SQL injection** — `{{interpolation}}` in SQL is always bound as a prepared-statement parameter.
- **XSS** — `mustache()`/`mdm()` auto-escape all context values; raw HTML requires explicit `{{{field}}}`.
- **CSRF** — per-session token verified on state-changing requests; set via `{{csrf:input}}` or `{{csrf:token}}`.
---
## Tooling
| Command | Purpose |
|---|---|
| `app_info` | View full app topology |
| `app_info resources` | List all resources |
| `app_info pipelines` | Inspect pipelines |
| `app_info events` | View pub/sub map |
| `app_info databases` | Inspect schemas |
| `app_debug` | Interactive pipeline-aware debugger in TUI |
| `unit_tests` | Criterion-based unit tests |
| `e2e_tests` | Playwright end-to-end tests |
| `app_build` | Build minimal production Docker image |
Telemetry (traces, logs, errors, profiling) auto-emitted per pipeline step via OpenTelemetry; visualized at port 4000.
---
## License
MACH is licensed under the LGPL.