From 2551109ffa0887eba77e87afa97ec4675a4db227 Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 7 Jun 2026 14:54:46 +0000 Subject: [PATCH] dream: hello + counter demos + 10 end-to-end tests Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/dream/conformance.sh | 3 ++ lib/dream/demos/counter.sx | 35 ++++++++++++++++ lib/dream/demos/hello.sx | 16 ++++++++ lib/dream/tests/demos.sx | 83 ++++++++++++++++++++++++++++++++++++++ plans/dream-on-sx.md | 15 ++++--- 5 files changed, 147 insertions(+), 5 deletions(-) create mode 100644 lib/dream/demos/counter.sx create mode 100644 lib/dream/demos/hello.sx create mode 100644 lib/dream/tests/demos.sx diff --git a/lib/dream/conformance.sh b/lib/dream/conformance.sh index 2748ccd3..2cd59de2 100644 --- a/lib/dream/conformance.sh +++ b/lib/dream/conformance.sh @@ -31,6 +31,8 @@ MODULES=( "lib/dream/websocket.sx" "lib/dream/static.sx" "lib/dream/run.sx" + "lib/dream/demos/hello.sx" + "lib/dream/demos/counter.sx" ) # Suites: NAME RUNNER-FN PATH @@ -44,6 +46,7 @@ SUITES=( "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" + "demos dream-dm-tests-run! lib/dream/tests/demos.sx" ) TMPFILE=$(mktemp); trap "rm -f $TMPFILE" EXIT diff --git a/lib/dream/demos/counter.sx b/lib/dream/demos/counter.sx new file mode 100644 index 00000000..4166d7f7 --- /dev/null +++ b/lib/dream/demos/counter.sx @@ -0,0 +1,35 @@ +;; lib/dream/demos/counter.sx — per-session visit counter (counter.ml). +;; Demonstrates the session middleware: each browser session keeps its own count. + +(define + dream-counter-handler + (fn + (req) + (let + ((n (+ 1 (or (dream-session-field req "count") 0)))) + (begin + (dream-set-session-field req "count" n) + (dream-html (str "

You have visited this page " n " time(s).

")))))) + +;; reset clears the session counter +(define + dream-counter-reset + (fn + (req) + (begin + (dream-set-session-field req "count" 0) + (dream-redirect "/")))) + +(define + dream-counter-app-with + (fn + (backend) + ((dream-sessions backend) + (dream-router + (list + (dream-get "/" dream-counter-handler) + (dream-post "/reset" dream-counter-reset)))))) + +(define dream-counter-app (dream-counter-app-with (dream-memory-sessions))) + +;; entry point: (dream-run (dream-counter-app-with (dream-memory-sessions))) diff --git a/lib/dream/demos/hello.sx b/lib/dream/demos/hello.sx new file mode 100644 index 00000000..0082dc25 --- /dev/null +++ b/lib/dream/demos/hello.sx @@ -0,0 +1,16 @@ +;; lib/dream/demos/hello.sx — the canonical Dream "Hello, World!" (hello.ml). +;; Dream.run (Dream.router [Dream.get "/" (fun _ -> Dream.html "Hello!")]). + +(define + dream-hello-app + (dream-router + (list + (dream-get "/" (fn (req) (dream-html "

Hello, World!

"))) + (dream-get + "/hello/:name" + (fn + (req) + (dream-html (str "

Hello, " (dream-param req "name") "!

"))))))) + +;; entry point (installs the handler on the host): +;; (dream-run dream-hello-app) diff --git a/lib/dream/tests/demos.sx b/lib/dream/tests/demos.sx new file mode 100644 index 00000000..7394dbd9 --- /dev/null +++ b/lib/dream/tests/demos.sx @@ -0,0 +1,83 @@ +;; lib/dream/tests/demos.sx — end-to-end demo apps exercising the full stack. + +(define dream-dm-pass 0) +(define dream-dm-fail 0) +(define dream-dm-fails (list)) + +(define + dream-dm-test + (fn + (name actual expected) + (if + (= actual expected) + (set! dream-dm-pass (+ dream-dm-pass 1)) + (begin + (set! dream-dm-fail (+ dream-dm-fail 1)) + (append! dream-dm-fails {:name name :actual actual :expected expected}))))) + +(define + dream-dm-req + (fn (method target headers) (dream-request method target headers ""))) + +;; ── hello ────────────────────────────────────────────────────────── +(dream-dm-test + "hello root" + (dream-resp-body (dream-hello-app (dream-dm-req "GET" "/" {}))) + "

Hello, World!

") +(dream-dm-test + "hello name" + (dream-resp-body + (dream-hello-app (dream-dm-req "GET" "/hello/Ada" {}))) + "

Hello, Ada!

") +(dream-dm-test + "hello content-type" + (dream-resp-header + (dream-hello-app (dream-dm-req "GET" "/" {})) + "content-type") + "text/html; charset=utf-8") + +;; ── counter (sessions) ───────────────────────────────────────────── +(define dream-dm-cbackend (dream-memory-sessions)) +(define dream-dm-capp (dream-counter-app-with dream-dm-cbackend)) + +;; first visit: no cookie -> count 1, session cookie set +(define dream-dm-c1 (dream-dm-capp (dream-dm-req "GET" "/" {}))) +(dream-dm-test + "counter first visit" + (dream-resp-body dream-dm-c1) + "

You have visited this page 1 time(s).

") +(dream-dm-test + "counter sets cookie" + (len (dream-resp-cookies dream-dm-c1)) + 1) + +;; subsequent visits with the cookie increment +(dream-dm-test + "counter second visit" + (dream-resp-body (dream-dm-capp (dream-dm-req "GET" "/" {:Cookie "dream.session=s1"}))) + "

You have visited this page 2 time(s).

") +(dream-dm-test + "counter third visit" + (dream-resp-body (dream-dm-capp (dream-dm-req "GET" "/" {:Cookie "dream.session=s1"}))) + "

You have visited this page 3 time(s).

") + +;; reset zeroes the counter then redirects +(define + dream-dm-reset + (dream-dm-capp (dream-dm-req "POST" "/reset" {:Cookie "dream.session=s1"}))) +(dream-dm-test + "counter reset redirects" + (dream-status dream-dm-reset) + 303) +(dream-dm-test + "counter after reset" + (dream-resp-body (dream-dm-capp (dream-dm-req "GET" "/" {:Cookie "dream.session=s1"}))) + "

You have visited this page 1 time(s).

") + +;; a different session is independent +(dream-dm-test + "counter distinct session" + (dream-resp-body (dream-dm-capp (dream-dm-req "GET" "/" {}))) + "

You have visited this page 1 time(s).

") + +(define dream-dm-tests-run! (fn () {:total (+ dream-dm-pass dream-dm-fail) :passed dream-dm-pass :failed dream-dm-fail :fails dream-dm-fails})) diff --git a/plans/dream-on-sx.md b/plans/dream-on-sx.md index c97ee1d6..7438d325 100644 --- a/plans/dream-on-sx.md +++ b/plans/dream-on-sx.md @@ -74,11 +74,11 @@ The five types: `request`, `response`, `handler = request -> response`, `middlew - `dream-send ws msg`, `dream-receive ws`, `dream-close ws`. - [x] **Static files:** `dream-static root-path` — serves files, ETags, range requests. - [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. - - `chat.ml` → `lib/dream/demos/chat.sx`: multi-room WebSocket chat. - - `todo.ml` → `lib/dream/demos/todo.sx`: CRUD list with forms + CSRF. +- [~] **Demos** in `lib/dream/demos/`: + - [x] `hello.ml` → `lib/dream/demos/hello.sx`: "Hello, World!" route. + - [x] `counter.ml` → `lib/dream/demos/counter.sx`: in-memory counter with sessions. + - [ ] `chat.ml` → `lib/dream/demos/chat.sx`: multi-room WebSocket chat. + - [ ] `todo.ml` → `lib/dream/demos/todo.sx`: CRUD list with forms + CSRF. - [ ] Tests in `lib/dream/tests/`: routing dispatch, middleware composition, session round-trip, CSRF accept/reject, flash read-after-write — 60+ tests. ## Stdlib additions Dream will need @@ -198,6 +198,11 @@ Confirm scope before starting; some of these may be addable as Dream-internal he 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. +- **2026-06-07 — Demos: hello + counter** (`lib/dream/demos/`, 10 tests). `hello.sx` + is the canonical router with a `:name` param route. `counter.sx` is a per-session + visit counter on the session middleware (+ a `/reset` POST that redirects), + demonstrating session isolation across browsers. End-to-end tests drive both apps + as the host would. chat (ws) + todo (forms+CSRF) next. ## Blockers