writeonce/docs/examples/web-app/main.wo
shoney.arickathil 308f7eec92 feat: web-app storefront + dep-relative use resolution (iter 16 Task 4)
- docs/examples/web-app: Product (@unique name, backlink orders) / Order
  (ref Product) as @table classes; handlers as Handler classes —
  ListProducts (ordered query -> JSON array), ShowProduct (unique-index
  probe, 404), CreateProduct (checked json.decode -> 400; @unique trap ->
  409), CreateOrder (FK insert), DeleteProduct (FK restrict trap -> 409);
  Auth middleware reads WA_TOKEN. Entry validates port + token honestly.
- The [deps] KEY is the module name `use` imports: hyphens are not
  identifier characters, so the app keys the dep `framework` while the
  repository keeps its long name (recorded in the manifest comment).
- driver fix (real gap the chain exposed): a dependency's INTERNAL `use`
  paths are written against its own root (`use http` inside the framework)
  but compile under `<depname>/...` — compile_image now prefixes dep files'
  use paths with the dep name (stdlib namespaces stay bare; a path already
  starting with the dep name is untouched).

Verified end to end through the full chain (temp git remote of the
framework, file:// substituted, fetch -> lock -> build -> serve): 401
without the token; [] empty list; 201 create; 409 duplicate (@unique);
400 malformed json; list/show payloads exact; 404 unknown product; 201
order; 409 delete-while-referenced (FK restrict) with the server still
serving; SIGTERM clean; the product survives a process restart (WAL
replay). Gates: woc-test 540/0, oop-e2e 88/0, deps-accept 8/0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 19:56:18 +02:00

117 lines
3.9 KiB
Text

-- web-app — the storefront: writeonce-framework (via [deps]) + @table
-- persistence. Every handler is a class satisfying Handler; the auth gate is
-- a Middleware; the data layer is the language's own database — no ORM, no
-- separate process, one binary.
use env
use json
use framework
use framework/http
use framework/router
-- decode target for POST /products, encode shape for every product answer
typedef ProductView = { name: Text, price: Int, stock: Int }
typedef NewOrder = { product: Text, qty: Int }
fn view_json(name: Text, price: Int, stock: Int) -> Text {
return json.encode(ProductView { name: name, price: price, stock: stock });
}
class Auth {
token: Text
fn before(req: Req) -> ?Resp {
let got = req.headers["authorization"];
if got == nil { return unauthorized(); }
if got != "Bearer ${self.token}" { return unauthorized(); }
return nil;
}
}
class ListProducts {
pad: Int
fn handle(req: Req) -> Resp {
let body = "[";
let first = true;
for p in from x in Product order by x.name select x {
if first == false { body = body .. ","; }
first = false;
body = body .. view_json(p.name, p.price, p.stock);
}
return ok_json(body .. "]");
}
}
class ShowProduct {
pad: Int
fn handle(req: Req) -> Resp {
let name = req.params["name"];
if name == nil { return bad_request("no name"); }
let hits = from p in Product where p.name == name take 1 select p;
if len(hits) == 0 { return not_found(); }
let p = hits[0];
return ok_json(view_json(p.name, p.price, p.stock));
}
}
class CreateProduct {
pad: Int
fn handle(req: Req) -> Resp {
let v = json.decode(req.body) as ProductView;
if v == nil { return bad_request("body must be {name, price, stock}"); }
let made = try insert Product { name: v.name, price: v.price, stock: v.stock }
catch (e) nil;
if made == nil { return conflict("product name already exists"); }
return created_json(view_json(v.name, v.price, v.stock));
}
}
class CreateOrder {
pad: Int
fn handle(req: Req) -> Resp {
let v = json.decode(req.body) as NewOrder;
if v == nil { return bad_request("body must be {product, qty}"); }
if v.qty < 1 { return bad_request("qty must be positive"); }
let hits = from p in Product where p.name == v.product take 1 select p;
if len(hits) == 0 { return not_found(); }
insert Order { product: hits[0], qty: v.qty };
return created_json("{\"ok\":true}");
}
}
class DeleteProduct {
pad: Int
fn handle(req: Req) -> Resp {
let name = req.params["name"];
if name == nil { return bad_request("no name"); }
let hits = from p in Product where p.name == name take 1 select p;
if len(hits) == 0 { return not_found(); }
let gone = try delete hits[0] catch (e) nil;
if gone == nil { return conflict("orders still reference this product"); }
return ok_json("{\"deleted\":true}");
}
}
fn main(args: multi Text) -> Int {
if len(args) < 1 {
print_err("usage: web-app <port> (WA_TOKEN and WO_DATA must be set)");
return 2;
}
let port = parse_int(args[0]);
if port == nil {
print_err("web-app: <port> must be a number");
return 2;
}
let token = env.get("WA_TOKEN");
if token == nil {
print_err("web-app: WA_TOKEN is required (the auth middleware's bearer token)");
return 2;
}
let app = App { middleware: [], routes: [] };
app.use_mw(Mw { m: Auth { token: token } });
app.add(Route { method: "GET", pattern: "/products", h: ListProducts { pad: 0 } });
app.add(Route { method: "GET", pattern: "/products/:name", h: ShowProduct { pad: 0 } });
app.add(Route { method: "POST", pattern: "/products", h: CreateProduct { pad: 0 } });
app.add(Route { method: "POST", pattern: "/orders", h: CreateOrder { pad: 0 } });
app.add(Route { method: "DELETE", pattern: "/products/:name", h: DeleteProduct { pad: 0 } });
return app.serve("127.0.0.1", port);
}