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:
parent
4c75ba4fbd
commit
20322d4905
5 changed files with 185 additions and 0 deletions
41
docs/examples/web-app/README.md
Normal file
41
docs/examples/web-app/README.md
Normal 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 "";
|
||||||
|
}
|
||||||
|
}
|
||||||
17
docs/examples/web-app/wo.toml
Normal file
17
docs/examples/web-app/wo.toml
Normal 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" }
|
||||||
43
docs/examples/writeonce-framework/README.md
Normal file
43
docs/examples/writeonce-framework/README.md
Normal 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.
|
||||||
74
docs/examples/writeonce-framework/http/types.wo
Normal file
74
docs/examples/writeonce-framework/http/types.wo
Normal 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: "" };
|
||||||
|
}
|
||||||
10
docs/examples/writeonce-framework/wo.toml
Normal file
10
docs/examples/writeonce-framework/wo.toml
Normal 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`.
|
||||||
Loading…
Reference in a new issue