feat(examples): framework skeleton + web-app scaffold (iter 16 Task 1)

- docs/examples/writeonce-framework: library project (no fn main) — wo.toml,
  README (what it is + the honest v1 limits + the proxy TLS/h2 story), and
  http/types.wo: pub Req/Resp records + the response builders (ok_text/
  ok_json/created/not_found/bad_request/unauthorized/conflict/server_error/
  redirect). Typechecks + emits entry-less via woc --emit (1767-byte image).
- docs/examples/web-app: manifest with the real [deps] entry (future GitHub
  URL as documentation; the gate substitutes a file:// remote) + README
  (routes table, run instructions, nginx h2-in-front sketch). Code lands
  with Task 4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
shoney.arickathil 2026-08-19 19:43:19 +02:00
parent 4c75ba4fbd
commit 20322d4905
5 changed files with 185 additions and 0 deletions

View file

@ -0,0 +1,41 @@
# web-app — the storefront sample
A small store: `Product`/`Order` as `@table` classes, JSON routes, one auth
middleware — built on [`writeonce-framework`](../writeonce-framework/), which
it imports **through `[deps]`** (iteration 15). This app is iteration 16's
acceptance workload: `just web-app` runs the whole chain — fetch → lock →
build → serve → curl matrix → restart persistence → SIGTERM.
## Routes
| Route | What |
| --- | --- |
| `GET /products` | list (JSON array) |
| `GET /products/:id` | one product or 404 |
| `POST /products` | create from a JSON body (`name`, `price`, `stock`); 400 on malformed JSON; 409 on a duplicate name (`@unique`) |
| `POST /orders` | create (`product`, `qty`); FK checked |
| `DELETE /products/:id` | 409 while orders reference it (FK restrict), 200 after |
Every request needs `authorization: Bearer <token>` (the auth middleware);
the token comes from the `WA_TOKEN` env var.
## Run
woc . # fetches deps, builds target/web-app
WA_TOKEN=secret WO_DATA=./data ./target/web-app 8080
## TLS / HTTP2
None here, deliberately: deploy behind nginx/caddy — the proxy terminates
TLS+ALPN and speaks h2 to browsers while this backend serves HTTP/1.1
keep-alive. Sketch:
server {
listen 443 ssl;
http2 on;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}

View file

@ -0,0 +1,17 @@
name = "web-app"
version = "0.1.0"
description = "Storefront sample: consumes writeonce-framework through [deps]; @table persistence; iteration 16's acceptance workload"
[runtime]
wo = ">= 0.1"
# No [build] runtime pinned (portable): an installed woc self-locates wovm;
# the in-repo gate passes WO_RUNTIME (log-watcher's precedent).
# The framework is a real dependency, never a relative path — extraction of
# the framework to its own repository changes only this URL. The acceptance
# gate (scripts/web-app-accept.sh) substitutes a run-time file:// remote
# built from docs/examples/writeonce-framework, so CI never needs the
# network and this repo never carries .wo-deps/wo.lock artifacts.
[deps]
writeonce-framework = { git = "https://github.com/shoneyj/writeonce-framework", rev = "v0.1.0" }

View file

@ -0,0 +1,43 @@
# writeonce-framework
A web framework **written in writeonce**, consumed as a `[deps]` dependency
(iteration 15). Spec: `docs/superpowers/specs/2026-08-18-web-framework-design.md` §B.
```toml
[deps]
writeonce-framework = { git = "https://github.com/shoneyj/writeonce-framework", rev = "v0.1.0" }
```
## What it is
- **HTTP/1.1 keep-alive** server core (`http/`): request parsing
(`Content-Length` bodies), response serialization, a blocking serve loop
that answers 400 to malformed requests, 500 to trapping handlers (and
survives), closes every fd, and honors SIGTERM.
- **Router** (`router/`): method + path table with `:param` captures into
`req.params`; first match wins; no match is the framework's 404.
- **Handlers without closures**: the language has no function values by
doctrine, so a route handler is a class satisfying the `Handler` interface
(`fn handle(req: Req) -> Resp`), dispatched structurally — a
non-conforming handler is a compile error (WO-E205). Middleware is its own
interface (`fn before(req: Req) -> ?Resp`; nil = continue, a `Resp`
short-circuits).
- **Data layer for free**: handlers use `@table` + the query surface
directly — durable, compiler-checked persistence in the same binary. No
ORM, no database server.
## Honest limits (v1, all deliberate)
- **Single-threaded, blocking** — one request at a time. Concurrency arrives
underneath this same surface with the shard/fiber iterations (8/11).
- **TLS: none, anywhere.** Deploy behind nginx/caddy; the proxy terminates
TLS+ALPN and gives browsers HTTP/2 while this backend speaks HTTP/1.1
keep-alive. See the web-app sample's README for the nginx sketch.
- `Content-Length` bodies only (no chunked encoding), no WebSockets/SSE,
JSON-first (no templates).
## The consuming sample
`docs/examples/web-app` — a small storefront importing this framework
through `[deps]`. Its acceptance (`just web-app`) exercises the whole chain:
fetch → lock → build → serve → durable restart.

View file

@ -0,0 +1,74 @@
-- http/types.wo — the request/response shapes every layer shares, plus the
-- response builders. Records only; parsing lives in http/parse.wo, the
-- serve loop in http/serve.wo, dispatch in router/ and app.wo.
pub typedef Req = {
method: Text, -- uppercased: GET, POST, ...
path: Text, -- decoded path, query stripped
params: map<Text, Text>, -- :param captures, filled by the router
query: map<Text, Text>, -- decoded query-string pairs
headers: map<Text, Text>, -- names lowercased on read
body: Text -- exactly Content-Length bytes ("" if none)
}
pub typedef Resp = {
status: Int,
headers: map<Text, Text>,
body: Text
}
-- ---- builders: every route answers through one of these ----------------
pub fn ok_text(body: Text) -> Resp {
let h: map<Text, Text> = {};
h["content-type"] = "text/plain; charset=utf-8";
return Resp { status: 200, headers: h, body: body };
}
pub fn ok_json(body: Text) -> Resp {
let h: map<Text, Text> = {};
h["content-type"] = "application/json";
return Resp { status: 200, headers: h, body: body };
}
pub fn created_json(body: Text) -> Resp {
let h: map<Text, Text> = {};
h["content-type"] = "application/json";
return Resp { status: 201, headers: h, body: body };
}
pub fn not_found() -> Resp {
let h: map<Text, Text> = {};
h["content-type"] = "application/json";
return Resp { status: 404, headers: h, body: "{\"error\":\"not found\"}" };
}
pub fn bad_request(msg: Text) -> Resp {
let h: map<Text, Text> = {};
h["content-type"] = "application/json";
return Resp { status: 400, headers: h, body: "{\"error\":\"${msg}\"}" };
}
pub fn unauthorized() -> Resp {
let h: map<Text, Text> = {};
h["content-type"] = "application/json";
return Resp { status: 401, headers: h, body: "{\"error\":\"unauthorized\"}" };
}
pub fn conflict(msg: Text) -> Resp {
let h: map<Text, Text> = {};
h["content-type"] = "application/json";
return Resp { status: 409, headers: h, body: "{\"error\":\"${msg}\"}" };
}
pub fn server_error() -> Resp {
let h: map<Text, Text> = {};
h["content-type"] = "application/json";
return Resp { status: 500, headers: h, body: "{\"error\":\"internal error\"}" };
}
pub fn redirect(location: Text) -> Resp {
let h: map<Text, Text> = {};
h["location"] = location;
return Resp { status: 302, headers: h, body: "" };
}

View file

@ -0,0 +1,10 @@
name = "writeonce-framework"
version = "0.1.0"
description = "A web framework written in writeonce: HTTP/1.1 keep-alive server core, router with :param captures, Handler/Middleware structural interfaces (iteration 16)"
[runtime]
wo = ">= 0.1"
# A LIBRARY project: no `fn main` here — the consuming app owns the entry.
# Apps import this repo through `wo.toml [deps]` (iteration 15) and
# `use writeonce-framework` / `use writeonce-framework/http` / `.../router`.