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>
3.8 KiB
3.8 KiB
dream-on-sx
OCaml's 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
(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,*-ordefaults,dream-accepts?/dream-wants-json?). - router —
dream-get/-post/-put/-delete/-patch/-head/-options/-any,dream-router,dream-scope(prefix + middleware),:nameparams +**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-sessionsfor tests,dream-perform-sessionsin production; - static files take an fs —
dream-memory-fsvsdream-static-perform-fs; - websockets take an io —
dream-mock-wsvsdream-ws-perform-io; dream-runtakes 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-cookieslist on the response so multipleSet-Cookieheaders 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).