Files
rose-ash/lib/dream/README.md
giles 7fb833f54c
Some checks are pending
Test, Build, and Deploy / test-build-deploy (push) Waiting to run
dream: api.sx facade (make-app/serve) + README documenting public surface + 9 tests
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:13:44 +00:00

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

  • typesdream-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?).
  • routerdream-get/-post/-put/-delete/-patch/-head/-options/-any, dream-router, dream-scope (prefix + middleware), :name params + ** catch-all, 405 + Allow, automatic HEAD.
  • middlewaredream-pipeline, dream-no-middleware, dream-logger, dream-content-type, dream-set-header, dream-tap-request.
  • sessiondream-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).
  • flashdream-flash, dream-add-flash-message, dream-flash-messages.
  • formdream-form (Ok/Err), dream-form-fields, dream-multipart, CSRF (dream-csrf / dream-csrf-protect / dream-csrf-token / dream-csrf-tag).
  • websocketdream-websocket, dream-send/-receive/-close/-broadcast.
  • staticdream-static (mime, ETags, 304, ranges, traversal guard).
  • errordream-catch, dream-status-text/-line, dream-status-page.
  • corsdream-cors, dream-cors-origin, dream-cors-with.
  • jsondream-json-encode/-parse, dream-json-value, dream-json-body.
  • run / apidream-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).