;; ========================================================================== ;; forms.sx — Server-side definition forms ;; ;; Platform-specific special forms for declaring handlers, pages, queries, ;; and actions. These parse &key parameter lists and create typed definition ;; objects that the server runtime uses for routing and execution. ;; ;; When SX moves to isomorphic execution, these forms will have different ;; platform bindings on client vs server. The spec stays the same — only ;; the constructors (make-handler-def, make-query-def, etc.) change. ;; ;; Platform functions required: ;; make-handler-def(name, params, body, env) → HandlerDef ;; make-query-def(name, params, doc, body, env) → QueryDef ;; make-action-def(name, params, doc, body, env) → ActionDef ;; make-page-def(name, slots, env) → PageDef ;; ========================================================================== ;; -------------------------------------------------------------------------- ;; Shared: parse (&key param1 param2 ...) → list of param name strings ;; -------------------------------------------------------------------------- (define parse-key-params (fn (params-expr) (let ((params (list)) (in-key false)) (for-each (fn (p) (when (= (type-of p) "symbol") (let ((name (symbol-name p))) (cond (= name "&key") (set! in-key true) in-key (append! params name) :else (append! params name))))) params-expr) params))) ;; -------------------------------------------------------------------------- ;; defhandler — (defhandler name (&key param...) body) ;; -------------------------------------------------------------------------- (define sf-defhandler (fn (args env) (let ((name-sym (first args)) (params-raw (nth args 1)) (body (nth args 2)) (name (symbol-name name-sym)) (params (parse-key-params params-raw))) (let ((hdef (make-handler-def name params body env))) (env-set! env (str "handler:" name) hdef) hdef)))) ;; -------------------------------------------------------------------------- ;; defquery — (defquery name (&key param...) "docstring" body) ;; -------------------------------------------------------------------------- (define sf-defquery (fn (args env) (let ((name-sym (first args)) (params-raw (nth args 1)) (name (symbol-name name-sym)) (params (parse-key-params params-raw)) ;; Optional docstring before body (has-doc (and (>= (len args) 4) (= (type-of (nth args 2)) "string"))) (doc (if has-doc (nth args 2) "")) (body (if has-doc (nth args 3) (nth args 2)))) (let ((qdef (make-query-def name params doc body env))) (env-set! env (str "query:" name) qdef) qdef)))) ;; -------------------------------------------------------------------------- ;; defaction — (defaction name (&key param...) "docstring" body) ;; -------------------------------------------------------------------------- (define sf-defaction (fn (args env) (let ((name-sym (first args)) (params-raw (nth args 1)) (name (symbol-name name-sym)) (params (parse-key-params params-raw)) (has-doc (and (>= (len args) 4) (= (type-of (nth args 2)) "string"))) (doc (if has-doc (nth args 2) "")) (body (if has-doc (nth args 3) (nth args 2)))) (let ((adef (make-action-def name params doc body env))) (env-set! env (str "action:" name) adef) adef)))) ;; -------------------------------------------------------------------------- ;; defpage — (defpage name :path "/..." :auth :public :content expr ...) ;; ;; Keyword-slot form: all values after the name are :key value pairs. ;; Values are stored as unevaluated AST — resolved at request time. ;; -------------------------------------------------------------------------- (define sf-defpage (fn (args env) (let ((name-sym (first args)) (name (symbol-name name-sym)) (slots {})) ;; Parse keyword slots from remaining args (let ((i 1) (max-i (len args))) (for-each (fn (idx) (when (and (< idx max-i) (= (type-of (nth args idx)) "keyword")) (when (< (+ idx 1) max-i) (dict-set! slots (keyword-name (nth args idx)) (nth args (+ idx 1)))))) (range 1 max-i 2))) (let ((pdef (make-page-def name slots env))) (env-set! env (str "page:" name) pdef) pdef)))) ;; ========================================================================== ;; Page Execution Semantics ;; ========================================================================== ;; ;; A PageDef describes what to render for a route. The host evaluates slots ;; at request time. This section specifies the data → content protocol that ;; every host must implement identically. ;; ;; Slots (all unevaluated AST): ;; :path — route pattern (string) ;; :auth — "public" | "login" | "admin" ;; :layout — layout reference + kwargs ;; :stream — boolean, opt into chunked transfer ;; :shell — immediate content (contains ~suspense placeholders) ;; :fallback — loading skeleton for single-stream mode ;; :data — IO expression producing bindings ;; :content — template expression evaluated with data bindings ;; :filter, :aside, :menu — additional content slots ;; ;; -------------------------------------------------------------------------- ;; Data Protocol ;; -------------------------------------------------------------------------- ;; ;; The :data expression is evaluated at request time. It returns one of: ;; ;; 1. A dict — single-stream mode (default). ;; Each key becomes an env binding (underscores → hyphens). ;; Then :content is evaluated once with those bindings. ;; Result resolves the "stream-content" suspense slot. ;; ;; 2. A sequence of dicts — multi-stream mode. ;; The host delivers items over time (async generator, channel, etc.). ;; Each dict: ;; - MUST contain "stream-id" → string matching a ~suspense :id ;; - Remaining keys become env bindings (underscores → hyphens) ;; - :content is re-evaluated with those bindings ;; - Result resolves the ~suspense slot matching "stream-id" ;; If "stream-id" is absent, defaults to "stream-content". ;; ;; The host is free to choose the timing mechanism: ;; Python — async generator (yield dicts at intervals) ;; Go — channel of dicts ;; Haskell — conduit / streaming ;; JS — async iterator ;; ;; The spec requires: ;; (a) Each item's bindings are isolated (fresh env per item) ;; (b) :content is evaluated independently for each item ;; (c) Resolution is incremental — each item resolves as it arrives ;; (d) "stream-id" routes to the correct ~suspense slot ;; ;; -------------------------------------------------------------------------- ;; Streaming Execution Order ;; -------------------------------------------------------------------------- ;; ;; When :stream is true: ;; ;; 1. Evaluate :shell (if present) → HTML for immediate content slot ;; :shell typically contains ~suspense placeholders with :fallback ;; 2. Render HTML shell with suspense placeholders → send to client ;; 3. Start :data evaluation concurrently with header resolution ;; 4. As each data item arrives: ;; a. Bind item keys into fresh env ;; b. Evaluate :content with those bindings → SX wire format ;; c. Send resolve script: __sxResolve(stream-id, sx) ;; 5. Close response when all items + headers have resolved ;; ;; Non-streaming pages evaluate :data then :content sequentially and ;; return the complete page in a single response. ;; ;; -------------------------------------------------------------------------- ;; Spec helpers for multi-stream data protocol ;; -------------------------------------------------------------------------- ;; Extract stream-id from a data chunk dict, defaulting to "stream-content" (define stream-chunk-id (fn (chunk) (if (has-key? chunk "stream-id") (get chunk "stream-id") "stream-content"))) ;; Remove stream-id from chunk, returning only the bindings (define stream-chunk-bindings (fn (chunk) (dissoc chunk "stream-id"))) ;; Normalize binding keys: underscore → hyphen (define normalize-binding-key (fn (key) (replace key "_" "-"))) ;; Bind a data chunk's keys into a fresh env (isolated per chunk) (define bind-stream-chunk (fn (chunk base-env) (let ((env (merge {} base-env)) (bindings (stream-chunk-bindings chunk))) (for-each (fn (key) (env-set! env (normalize-binding-key key) (get bindings key))) (keys bindings)) env))) ;; Validate a multi-stream data result: must be a list of dicts (define validate-stream-data (fn (data) (and (= (type-of data) "list") (every? (fn (item) (= (type-of item) "dict")) data))))