Some checks are pending
Test, Build, and Deploy / test-build-deploy (push) Waiting to run
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
80 lines
3.8 KiB
Markdown
80 lines
3.8 KiB
Markdown
# dream-on-sx
|
|
|
|
OCaml's [Dream](https://aantron.github.io/dream/) web framework, reimplemented in
|
|
**plain SX** on the CEK evaluator. Dream is the cleanest middleware-shaped HTTP
|
|
framework in any language, and it maps onto SX with almost no impedance:
|
|
|
|
| Dream | SX |
|
|
|-------|-----|
|
|
| `handler = request -> response promise` | `(fn (req) … (perform …))` |
|
|
| `middleware = handler -> handler` | `(fn (next) (fn (req) …))` |
|
|
| `m1 @@ m2 @@ handler` | `(m1 (m2 handler))` — left fold |
|
|
| `Dream.run handler` | `(dream-run handler)` → `(perform (:http/listen …))` |
|
|
|
|
There are five types — **request, response, route**, and (as plain functions)
|
|
**handler** and **middleware**. Everything else is a function over them.
|
|
|
|
## Quickstart
|
|
|
|
```lisp
|
|
(dream-run
|
|
(dream-make-app
|
|
(list
|
|
(dream-get "/" (fn (req) (dream-html "<h1>Hello, World!</h1>")))
|
|
(dream-get "/hello/:name"
|
|
(fn (req) (dream-text (str "Hi, " (dream-param req "name"))))))))
|
|
```
|
|
|
|
`dream-make-app` wraps the router in the default stack (error catch + content-type).
|
|
`dream-run` installs the root handler on the existing SX HTTP server — it does **not**
|
|
open its own socket.
|
|
|
|
## Public surface
|
|
|
|
- **types** — `dream-request`/`dream-response`/`dream-route`, accessors
|
|
(`dream-method`/`-path`/`-body`/`-header`/`-query-param`/`-param`), smart
|
|
constructors (`dream-html`/`-text`/`-json`/`-empty`/`-not-found`/`-redirect`),
|
|
convenience (`dream-queries`, `*-or` defaults, `dream-accepts?`/`dream-wants-json?`).
|
|
- **router** — `dream-get`/`-post`/`-put`/`-delete`/`-patch`/`-head`/`-options`/`-any`,
|
|
`dream-router`, `dream-scope` (prefix + middleware), `:name` params + `**` catch-all,
|
|
405 + `Allow`, automatic HEAD.
|
|
- **middleware** — `dream-pipeline`, `dream-no-middleware`, `dream-logger`,
|
|
`dream-content-type`, `dream-set-header`, `dream-tap-request`.
|
|
- **session** — `dream-sessions` / `dream-sessions-signed`, `dream-session-field` /
|
|
`dream-set-session-field` / `dream-session-all` / `dream-invalidate-session`; cookie
|
|
helpers (`dream-cookie`, `dream-set-cookie`, `dream-cookie-sign`/`-unsign`).
|
|
- **flash** — `dream-flash`, `dream-add-flash-message`, `dream-flash-messages`.
|
|
- **form** — `dream-form` (Ok/Err), `dream-form-fields`, `dream-multipart`, CSRF
|
|
(`dream-csrf` / `dream-csrf-protect` / `dream-csrf-token` / `dream-csrf-tag`).
|
|
- **websocket** — `dream-websocket`, `dream-send`/`-receive`/`-close`/`-broadcast`.
|
|
- **static** — `dream-static` (mime, ETags, 304, ranges, traversal guard).
|
|
- **error** — `dream-catch`, `dream-status-text`/`-line`, `dream-status-page`.
|
|
- **cors** — `dream-cors`, `dream-cors-origin`, `dream-cors-with`.
|
|
- **json** — `dream-json-encode`/`-parse`, `dream-json-value`, `dream-json-body`.
|
|
- **run / api** — `dream-run`/`-port`/`-opts`, `dream-app`, `dream-make-app`,
|
|
`dream-serve`.
|
|
|
|
## Testing story
|
|
|
|
Every effectful concern is **dependency-injected**, so the whole framework is testable
|
|
without a running host:
|
|
|
|
- sessions take a backend `(fn (op) …)` — `dream-memory-sessions` for tests,
|
|
`dream-perform-sessions` in production;
|
|
- static files take an fs — `dream-memory-fs` vs `dream-static-perform-fs`;
|
|
- websockets take an io — `dream-mock-ws` vs `dream-ws-perform-io`;
|
|
- `dream-run` takes a listen transport (`dream-run-with`).
|
|
|
|
Run the suite: `bash lib/dream/conformance.sh` (367 tests, 14 suites).
|
|
|
|
## Notes & caveats
|
|
|
|
- Headers are dicts with **lowercased string keys** (in SX keywords *are* strings, so
|
|
`:content-type` == `"content-type"`).
|
|
- Outgoing cookies accumulate in a `:set-cookies` list on the response so multiple
|
|
`Set-Cookie` headers don't collide.
|
|
- The CSRF/cookie/ETag signing uses a pure-SX keyed hash — **not cryptographic**.
|
|
Production should inject a host HMAC (`dream-csrf-with`, and the signed-session
|
|
secret path).
|
|
- JSON and multipart are in-memory (not streaming).
|