230 lines
9.1 KiB
Plaintext
230 lines
9.1 KiB
Plaintext
;; ==========================================================================
|
|
;; 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))))
|