diff --git a/lib/dream/tests/types.sx b/lib/dream/tests/types.sx index 5b4f9893..d4a8dcdc 100644 --- a/lib/dream/tests/types.sx +++ b/lib/dream/tests/types.sx @@ -1,4 +1,4 @@ -;; lib/dream/tests/types.sx — request/response/route records. +;; lib/dream/tests/types.sx — request/response/route records + convenience. (define dream-ty-pass 0) (define dream-ty-fail 0) @@ -66,6 +66,57 @@ (dream-ty-test "with-params a" (dream-param dream-ty-req3 "a") "1") (dream-ty-test "with-params b" (dream-param dream-ty-req3 "b") "2") +;; ── request convenience ──────────────────────────────────────────── +(dream-ty-test "queries dict" (dream-queries dream-ty-req) {:x "1" :tab "info"}) +(dream-ty-test + "query-or present" + (dream-query-param-or dream-ty-req "tab" "def") + "info") +(dream-ty-test + "query-or default" + (dream-query-param-or dream-ty-req "missing" "def") + "def") +(dream-ty-test "has-query yes" (dream-has-query? dream-ty-req "tab") true) +(dream-ty-test "has-query no" (dream-has-query? dream-ty-req "nope") false) +(dream-ty-test + "header-or present" + (dream-header-or dream-ty-req "x-token" "d") + "abc") +(dream-ty-test + "header-or default" + (dream-header-or dream-ty-req "x-absent" "d") + "d") +(dream-ty-test + "has-header yes" + (dream-has-header? dream-ty-req "Content-Type") + true) +(dream-ty-test + "has-header no" + (dream-has-header? dream-ty-req "x-absent") + false) +(dream-ty-test "param-or default" (dream-param-or dream-ty-req "id" "0") "0") +(dream-ty-test + "param-or present" + (dream-param-or dream-ty-req2 "id" "0") + "42") +(dream-ty-test + "content-type-of" + (dream-content-type-of dream-ty-req) + "text/html") +(dream-ty-test "method-is yes" (dream-method-is? dream-ty-req "get") true) +(dream-ty-test "method-is no" (dream-method-is? dream-ty-req "post") false) +(define dream-ty-jreq (dream-request "GET" "/" {:Accept "application/json, text/html"} "")) +(dream-ty-test + "accepts json" + (dream-accepts? dream-ty-jreq "application/json") + true) +(dream-ty-test + "accepts missing" + (dream-accepts? dream-ty-req "application/json") + false) +(dream-ty-test "wants-json yes" (dream-wants-json? dream-ty-jreq) true) +(dream-ty-test "wants-json no" (dream-wants-json? dream-ty-req) false) + ;; ── response construction ────────────────────────────────────────── (dream-ty-test "html status" (dream-status (dream-html "

")) 200) (dream-ty-test "html body" (dream-resp-body (dream-html "

")) "

") diff --git a/lib/dream/types.sx b/lib/dream/types.sx index 75d93aba..2e31f3fb 100644 --- a/lib/dream/types.sx +++ b/lib/dream/types.sx @@ -93,6 +93,35 @@ (keys more))))) (define dream-set-body (fn (req body) (assoc req :body body))) +;; ── request convenience ──────────────────────────────────────────── +(define dream-queries (fn (req) (get req :query))) +(define + dream-query-param-or + (fn (req name default) (or (dream-query-param req name) default))) +(define dream-has-query? (fn (req name) (has-key? (get req :query) name))) +(define + dream-header-or + (fn (req name default) (or (dream-header req name) default))) +(define + dream-has-header? + (fn (req name) (has-key? (get req :headers) (lower name)))) +(define + dream-param-or + (fn (req name default) (or (dream-param req name) default))) +(define dream-has-param? (fn (req name) (has-key? (get req :params) name))) +(define dream-content-type-of (fn (req) (dream-header req "content-type"))) +(define dream-method-is? (fn (req m) (= (dream-method req) (upper m)))) +(define + dream-accepts? + (fn + (req mime) + (let + ((a (dream-header req "accept"))) + (if a (contains? a mime) false)))) +(define + dream-wants-json? + (fn (req) (dream-accepts? req "application/json"))) + ;; ── response ─────────────────────────────────────────────────────── (define dream-response (fn (status headers body) {:body body :headers (dr/normalize-headers headers) :status status})) diff --git a/plans/dream-on-sx.md b/plans/dream-on-sx.md index 5a7bbdf0..89344d77 100644 --- a/plans/dream-on-sx.md +++ b/plans/dream-on-sx.md @@ -119,6 +119,11 @@ with extensions + hardening below. wrong-secret cookie yields a fresh session instead of a hijack. `dream-cookie-sign` / `dream-cookie-unsign` (keyed hash; same not-cryptographic caveat — inject a host HMAC in production). Plain `dream-sessions` unchanged for the no-secret case. +- **2026-06-07 — Ext: query/header convenience** (`lib/dream/types.sx`, types suite + 41→59, 358 total). `dream-queries`, `dream-query-param-or` / `dream-header-or` / + `dream-param-or` (defaults), `dream-has-query?` / `-header?` / `-param?`, + `dream-content-type-of`, `dream-method-is?`, `dream-accepts?` / `dream-wants-json?` + (Accept-header content negotiation). ## Extensions (post-roadmap) @@ -131,7 +136,7 @@ The five-types core is complete; these harden it toward a production HTTP front - [x] **Error-handling middleware** (`dream-catch` / custom 500 templates; `guard`-based). - [x] **Signed session cookies** (`dream-sessions-signed` — tamper-evident sid). - [x] **JSON helpers** (encode + recursive-descent parse, pure SX). -- [ ] **Query/header convenience** (`dream-queries`, defaults). +- [x] **Query/header convenience** (`dream-queries`, `*-or` defaults, `dream-accepts?`). - [ ] **`api.sx` facade + README** — single load point listing the public surface. ## Stdlib additions Dream will need