- http/multipart.wo: RFC 7578 whole-body parsing within BODY_MAX —
boundary from the raw content-type (quoted or bare, key
case-insensitive), parts split on --boundary, each part = headers,
blank line, content; filename + per-part content-type kept (lowercased)
- strict malformed-is-nil: no closing --boundary-- marker, a part
without content-disposition, missing blank line, no boundary param,
wrong media type — all nil, the caller's 400
- part_named(parts, name): first matching field's content, caller-owned
- web-app CreateProduct now accepts multipart/form/JSON (curl -F shape)
into the shared insert path
- probe 13/13 + 3x reuse loop (fields, crlf-in-content, quoted boundary,
file part, zero-part close, five malformed shapes) release + ASan
- gate grows 19 -> 21: multipart create 201, missing closing marker 400
- README: multipart row ✅ (all three body hooks done), limits updated;
story 16 + board record the landing
- gates: web-app 21/0, oop-e2e 89/0, deps-accept 8/0, log-watcher 7/0,
employee 8/0, woc-test green
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
145 lines
5.4 KiB
Text
145 lines
5.4 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 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));
|
|
}
|
|
}
|
|
|
|
-- Accepts THREE bodies: multipart/form-data (curl -F), a form post
|
|
-- (application/x-www-form-urlencoded), and JSON — same insert either way.
|
|
fn create_product(name: Text, price: Int, stock: Int) -> Resp {
|
|
let made = try insert Product { name: name, price: price, stock: stock }
|
|
catch (e) nil;
|
|
if made == nil { return conflict("product name already exists"); }
|
|
return created_json(view_json(name, price, stock));
|
|
}
|
|
|
|
class CreateProduct {
|
|
pad: Int
|
|
fn handle(req: Req) -> Resp {
|
|
if media_type(req) == "multipart/form-data" {
|
|
let ps = multipart_parts(req);
|
|
if ps == nil { return bad_request("unreadable multipart body"); }
|
|
let name = part_named(ps, "name");
|
|
if name == nil { return bad_request("multipart needs name, price, stock"); }
|
|
let pstr = part_named(ps, "price");
|
|
if pstr == nil { return bad_request("multipart needs name, price, stock"); }
|
|
let sstr = part_named(ps, "stock");
|
|
if sstr == nil { return bad_request("multipart needs name, price, stock"); }
|
|
let price = parse_int(pstr);
|
|
if price == nil { return bad_request("price must be a number"); }
|
|
let stock = parse_int(sstr);
|
|
if stock == nil { return bad_request("stock must be a number"); }
|
|
return create_product(name, price, stock);
|
|
}
|
|
if media_type(req) == "application/x-www-form-urlencoded" {
|
|
let f = form_values(req);
|
|
if f == nil { return bad_request("unreadable form body"); }
|
|
let name = f["name"];
|
|
if name == nil { return bad_request("form needs name, price, stock"); }
|
|
let ps = f["price"];
|
|
if ps == nil { return bad_request("form needs name, price, stock"); }
|
|
let ss = f["stock"];
|
|
if ss == nil { return bad_request("form needs name, price, stock"); }
|
|
let price = parse_int(ps);
|
|
if price == nil { return bad_request("price must be a number"); }
|
|
let stock = parse_int(ss);
|
|
if stock == nil { return bad_request("stock must be a number"); }
|
|
return create_product(name, price, stock);
|
|
}
|
|
let v = json.decode(req.body) as ProductView;
|
|
if v == nil { return bad_request("body must be {name, price, stock}"); }
|
|
return create_product(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: [] };
|
|
-- the framework's Bearer mechanism: constant-time compare, principal
|
|
-- attached to req.principal for handlers that want "who is this"
|
|
app.use_mw(Mw { m: BearerAuth { token: token, principal: "api" } });
|
|
app.get("/products", ListProducts { pad: 0 });
|
|
app.get("/products/:name", ShowProduct { pad: 0 });
|
|
app.post("/products", CreateProduct { pad: 0 });
|
|
app.post("/orders", CreateOrder { pad: 0 });
|
|
app.delete_("/products/:name", DeleteProduct { pad: 0 });
|
|
return app.serve("127.0.0.1", port);
|
|
}
|