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