dream: dream-run entry point + request/response host adapter + 20 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 59s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 59s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,7 @@ MODULES=(
|
||||
"lib/dream/form.sx"
|
||||
"lib/dream/websocket.sx"
|
||||
"lib/dream/static.sx"
|
||||
"lib/dream/run.sx"
|
||||
)
|
||||
|
||||
# Suites: NAME RUNNER-FN PATH
|
||||
@@ -42,6 +43,7 @@ SUITES=(
|
||||
"form dream-fo-tests-run! lib/dream/tests/form.sx"
|
||||
"websocket dream-ws-tests-run! lib/dream/tests/websocket.sx"
|
||||
"static dream-st-tests-run! lib/dream/tests/static.sx"
|
||||
"run dream-rn-tests-run! lib/dream/tests/run.sx"
|
||||
)
|
||||
|
||||
TMPFILE=$(mktemp); trap "rm -f $TMPFILE" EXIT
|
||||
|
||||
42
lib/dream/run.sx
Normal file
42
lib/dream/run.sx
Normal file
@@ -0,0 +1,42 @@
|
||||
;; lib/dream/run.sx — Dream-on-SX entry point.
|
||||
;; dream-run installs a root handler into the existing SX HTTP server via
|
||||
;; (perform (:http/listen …)) — it does NOT implement its own socket loop. The
|
||||
;; host invokes the installed app per request with a raw request dict; the app
|
||||
;; adapts it to a dream-request, runs the handler, and serialises the response
|
||||
;; (status/headers/body/set-cookies, or a websocket upgrade). Depends on types.sx
|
||||
;; + websocket.sx. The listen transport is injectable for testing.
|
||||
|
||||
;; ── response serialisation for the host ────────────────────────────
|
||||
(define
|
||||
dr/serialize-response
|
||||
(fn (resp) (if (dream-websocket? resp) {:websocket (dream-ws-handler resp) :body "" :headers (dream-headers resp) :status 101 :set-cookies (list)} {:body (dream-resp-body resp) :headers (dream-headers resp) :status (dream-status resp) :set-cookies (dream-resp-cookies resp)})))
|
||||
|
||||
;; ── the app: raw host request -> serialised response ───────────────
|
||||
(define
|
||||
dream-app
|
||||
(fn
|
||||
(handler)
|
||||
(fn
|
||||
(raw)
|
||||
(let
|
||||
((req (dream-request (or (get raw :method) "GET") (or (get raw :target) (or (get raw :path) "/")) (or (get raw :headers) {}) (or (get raw :body) ""))))
|
||||
(dr/serialize-response (dream-coerce-response (handler req)))))))
|
||||
|
||||
;; ── dream-run ──────────────────────────────────────────────────────
|
||||
(define dream-default-port 8080)
|
||||
|
||||
(define dream-run-with (fn (listen handler opts) (listen {:op "http/listen" :port (or (get opts :port) dream-default-port) :app (dream-app handler) :host (or (get opts :host) "0.0.0.0")})))
|
||||
|
||||
(define dream-perform-listen (fn (op) (perform op)))
|
||||
|
||||
(define
|
||||
dream-run
|
||||
(fn (handler) (dream-run-with dream-perform-listen handler {})))
|
||||
(define
|
||||
dream-run-port
|
||||
(fn
|
||||
(handler port)
|
||||
(dream-run-with dream-perform-listen handler {:port port})))
|
||||
(define
|
||||
dream-run-opts
|
||||
(fn (handler opts) (dream-run-with dream-perform-listen handler opts)))
|
||||
123
lib/dream/tests/run.sx
Normal file
123
lib/dream/tests/run.sx
Normal file
@@ -0,0 +1,123 @@
|
||||
;; lib/dream/tests/run.sx — app adapter + dream-run wiring.
|
||||
|
||||
(define dream-rn-pass 0)
|
||||
(define dream-rn-fail 0)
|
||||
(define dream-rn-fails (list))
|
||||
|
||||
(define
|
||||
dream-rn-test
|
||||
(fn
|
||||
(name actual expected)
|
||||
(if
|
||||
(= actual expected)
|
||||
(set! dream-rn-pass (+ dream-rn-pass 1))
|
||||
(begin
|
||||
(set! dream-rn-fail (+ dream-rn-fail 1))
|
||||
(append! dream-rn-fails {:name name :actual actual :expected expected})))))
|
||||
|
||||
;; ── app adapter: raw -> serialised response ────────────────────────
|
||||
(define
|
||||
dream-rn-router
|
||||
(dream-router
|
||||
(list
|
||||
(dream-get "/" (fn (req) (dream-text "home")))
|
||||
(dream-get
|
||||
"/u/:id"
|
||||
(fn (req) (dream-text (str "u=" (dream-param req "id")))))
|
||||
(dream-post "/echo" (fn (req) (dream-text (dream-body req)))))))
|
||||
(define dream-rn-app (dream-app dream-rn-router))
|
||||
|
||||
(define dream-rn-r1 (dream-rn-app {:method "GET" :target "/"}))
|
||||
(dream-rn-test "serialised status" (get dream-rn-r1 :status) 200)
|
||||
(dream-rn-test "serialised body" (get dream-rn-r1 :body) "home")
|
||||
(dream-rn-test
|
||||
"serialised content-type"
|
||||
(get (get dream-rn-r1 :headers) "content-type")
|
||||
"text/plain; charset=utf-8")
|
||||
(dream-rn-test
|
||||
"serialised set-cookies empty"
|
||||
(get dream-rn-r1 :set-cookies)
|
||||
(list))
|
||||
|
||||
(dream-rn-test
|
||||
"adapts target+params"
|
||||
(get (dream-rn-app {:method "GET" :target "/u/42"}) :body)
|
||||
"u=42")
|
||||
(dream-rn-test "adapts body" (get (dream-rn-app {:body "ping" :method "POST" :target "/echo"}) :body) "ping")
|
||||
(dream-rn-test
|
||||
"method defaults to GET"
|
||||
(get (dream-rn-app {:target "/"}) :body)
|
||||
"home")
|
||||
(dream-rn-test
|
||||
"missing target -> /"
|
||||
(get (dream-rn-app {:method "GET"}) :status)
|
||||
200)
|
||||
(dream-rn-test
|
||||
"unknown route 404"
|
||||
(get (dream-rn-app {:method "GET" :target "/nope"}) :status)
|
||||
404)
|
||||
|
||||
;; bare-string handler is coerced
|
||||
(define dream-rn-bare (dream-app (fn (req) "plain")))
|
||||
(dream-rn-test
|
||||
"coerces bare string status"
|
||||
(get (dream-rn-bare {:target "/"}) :status)
|
||||
200)
|
||||
(dream-rn-test
|
||||
"coerces bare string body"
|
||||
(get (dream-rn-bare {:target "/"}) :body)
|
||||
"plain")
|
||||
|
||||
;; ── set-cookies flow through (session middleware) ──────────────────
|
||||
(define
|
||||
dream-rn-sess-app
|
||||
(dream-app
|
||||
((dream-sessions (dream-memory-sessions))
|
||||
(fn (req) (dream-text "ok")))))
|
||||
(define dream-rn-sess-r (dream-rn-sess-app {:method "GET" :target "/"}))
|
||||
(dream-rn-test
|
||||
"session set-cookie present"
|
||||
(len (get dream-rn-sess-r :set-cookies))
|
||||
1)
|
||||
(dream-rn-test
|
||||
"session cookie content"
|
||||
(contains? (first (get dream-rn-sess-r :set-cookies)) "dream.session=")
|
||||
true)
|
||||
|
||||
;; ── websocket upgrade serialisation ────────────────────────────────
|
||||
(define
|
||||
dream-rn-ws-app
|
||||
(dream-app (dream-websocket (fn (ws) (dream-close ws)))))
|
||||
(define dream-rn-ws-r (dream-rn-ws-app {:method "GET" :target "/ws"}))
|
||||
(dream-rn-test "ws upgrade status 101" (get dream-rn-ws-r :status) 101)
|
||||
(dream-rn-test
|
||||
"ws handler carried"
|
||||
(not (nil? (get dream-rn-ws-r :websocket)))
|
||||
true)
|
||||
|
||||
;; ── dream-run wiring (mock listen captures the op) ─────────────────
|
||||
(define dream-rn-captured nil)
|
||||
(define
|
||||
dream-rn-listen
|
||||
(fn (op) (begin (set! dream-rn-captured op) :listening)))
|
||||
(define
|
||||
dream-rn-result
|
||||
(dream-run-with dream-rn-listen dream-rn-router {:port 9000}))
|
||||
(dream-rn-test "listen returns" dream-rn-result :listening)
|
||||
(dream-rn-test "listen op kind" (get dream-rn-captured :op) "http/listen")
|
||||
(dream-rn-test "listen port" (get dream-rn-captured :port) 9000)
|
||||
(dream-rn-test
|
||||
"default port"
|
||||
(get
|
||||
(begin
|
||||
(dream-run-with dream-rn-listen dream-rn-router {})
|
||||
dream-rn-captured)
|
||||
:port)
|
||||
8080)
|
||||
;; the captured app is runnable
|
||||
(dream-rn-test
|
||||
"captured app serves"
|
||||
(get ((get dream-rn-captured :app) {:method "GET" :target "/"}) :body)
|
||||
"home")
|
||||
|
||||
(define dream-rn-tests-run! (fn () {:total (+ dream-rn-pass dream-rn-fail) :passed dream-rn-pass :failed dream-rn-fail :fails dream-rn-fails}))
|
||||
@@ -73,7 +73,7 @@ The five types: `request`, `response`, `handler = request -> response`, `middlew
|
||||
- `dream-websocket handler` — upgrades request; handler `(fn (ws) ...)`.
|
||||
- `dream-send ws msg`, `dream-receive ws`, `dream-close ws`.
|
||||
- [x] **Static files:** `dream-static root-path` — serves files, ETags, range requests.
|
||||
- [ ] **`dream-run`**: wires root handler into SX's `perform (:http-listen ...)`.
|
||||
- [x] **`dream-run`**: wires root handler into SX's `perform (:http-listen ...)`.
|
||||
- [ ] **Demos** in `lib/dream/demos/`:
|
||||
- `hello.ml` → `lib/dream/demos/hello.sx`: "Hello, World!" route.
|
||||
- `counter.ml` → `lib/dream/demos/counter.sx`: in-memory counter with sessions.
|
||||
@@ -189,6 +189,15 @@ Confirm scope before starting; some of these may be addable as Dream-internal he
|
||||
unsatisfiable → 416). `..`/absolute path traversal → 403; missing → 404; full
|
||||
responses advertise `Accept-Ranges`. Filesystem is injectable —
|
||||
`dream-static-perform-fs` (host) vs `dream-memory-fs` (in-memory map for tests).
|
||||
- **2026-06-07 — dream-run** (`lib/dream/run.sx`, 20 tests). `dream-run handler`
|
||||
installs the root handler via `(perform (:http/listen {:port :host :app …}))` — no
|
||||
socket code, it wraps the existing server. `dream-app handler` is the adapter the
|
||||
host invokes per request: raw `{:method :target :headers :body}` → `dream-request`
|
||||
→ handler → serialised `{:status :headers :body :set-cookies}`, or a `{:status 101
|
||||
:websocket …}` upgrade. Bare-string handlers coerced; method defaults to GET;
|
||||
set-cookies (from session/flash) flow through. Listen transport injectable
|
||||
(`dream-run-with`) so the full wiring is tested with a mock that captures the op and
|
||||
re-runs the captured app. `dream-run-port` / `dream-run-opts` variants.
|
||||
|
||||
## Blockers
|
||||
|
||||
|
||||
Reference in New Issue
Block a user