Migrate remaining 7 ref endpoints to SX, add :returns type annotations
Add 14 new IO primitives to boundary.sx: web interop (request-form, request-json, request-header, request-content-type, request-args-all, request-form-all, request-headers-all, request-file-name), response manipulation (set-response-header, set-response-status), ephemeral state (state-get, state-set!), and timing (now, sleep). All 19 reference handlers now have :returns type annotations using types.sx vocabulary. Response meta (headers/status) flows through context vars, applied by register_route_handlers after execution. Only SSE endpoint remains in Python (async generator paradigm). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -239,10 +239,19 @@ def register_route_handlers(app_or_bp: Any, service_name: str) -> int:
|
|||||||
|
|
||||||
async def _route_view(_h=_hdef, **path_kwargs):
|
async def _route_view(_h=_hdef, **path_kwargs):
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
|
from shared.sx.primitives_io import reset_response_meta, get_response_meta
|
||||||
|
reset_response_meta()
|
||||||
args = dict(request.args)
|
args = dict(request.args)
|
||||||
args.update(path_kwargs)
|
args.update(path_kwargs)
|
||||||
result = await execute_handler(_h, service_name, args=args)
|
result = await execute_handler(_h, service_name, args=args)
|
||||||
return sx_response(result)
|
resp = sx_response(result)
|
||||||
|
meta = get_response_meta()
|
||||||
|
if meta:
|
||||||
|
if meta.get("status"):
|
||||||
|
resp.status_code = meta["status"]
|
||||||
|
for k, v in meta.get("headers", {}).items():
|
||||||
|
resp.headers[k] = v
|
||||||
|
return resp
|
||||||
|
|
||||||
endpoint = f"sx_route_{name}"
|
endpoint = f"sx_route_{name}"
|
||||||
view_fn = _route_view
|
view_fn = _route_view
|
||||||
|
|||||||
@@ -46,6 +46,13 @@ _handler_service: contextvars.ContextVar[Any] = contextvars.ContextVar(
|
|||||||
"_handler_service", default=None
|
"_handler_service", default=None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_response_meta: contextvars.ContextVar[dict | None] = contextvars.ContextVar(
|
||||||
|
"_response_meta", default=None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ephemeral per-process state — resets on restart. For demos/testing only.
|
||||||
|
_ephemeral_state: dict[str, Any] = {}
|
||||||
|
|
||||||
|
|
||||||
def set_handler_service(service_obj: Any) -> None:
|
def set_handler_service(service_obj: Any) -> None:
|
||||||
"""Bind the local domain service for ``(service ...)`` primitive calls."""
|
"""Bind the local domain service for ``(service ...)`` primitive calls."""
|
||||||
@@ -57,6 +64,16 @@ def get_handler_service() -> Any:
|
|||||||
return _handler_service.get(None)
|
return _handler_service.get(None)
|
||||||
|
|
||||||
|
|
||||||
|
def reset_response_meta() -> None:
|
||||||
|
"""Reset response meta for a new request."""
|
||||||
|
_response_meta.set(None)
|
||||||
|
|
||||||
|
|
||||||
|
def get_response_meta() -> dict | None:
|
||||||
|
"""Get response meta (headers/status) set by handler IO primitives."""
|
||||||
|
return _response_meta.get(None)
|
||||||
|
|
||||||
|
|
||||||
class RequestContext:
|
class RequestContext:
|
||||||
"""Per-request context provided to I/O primitives."""
|
"""Per-request context provided to I/O primitives."""
|
||||||
__slots__ = ("user", "is_htmx", "extras")
|
__slots__ = ("user", "is_htmx", "extras")
|
||||||
@@ -372,6 +389,105 @@ async def _io_request_content_type(
|
|||||||
return request.content_type or NIL
|
return request.content_type or NIL
|
||||||
|
|
||||||
|
|
||||||
|
@register_io_handler("request-args-all")
|
||||||
|
async def _io_request_args_all(
|
||||||
|
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||||
|
) -> dict:
|
||||||
|
"""``(request-args-all)`` → all query params as dict."""
|
||||||
|
from quart import request
|
||||||
|
return dict(request.args)
|
||||||
|
|
||||||
|
|
||||||
|
@register_io_handler("request-form-all")
|
||||||
|
async def _io_request_form_all(
|
||||||
|
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||||
|
) -> dict:
|
||||||
|
"""``(request-form-all)`` → all form fields as dict."""
|
||||||
|
from quart import request
|
||||||
|
form = await request.form
|
||||||
|
return dict(form)
|
||||||
|
|
||||||
|
|
||||||
|
@register_io_handler("request-headers-all")
|
||||||
|
async def _io_request_headers_all(
|
||||||
|
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||||
|
) -> dict:
|
||||||
|
"""``(request-headers-all)`` → all headers as dict (lowercase keys)."""
|
||||||
|
from quart import request
|
||||||
|
return {k.lower(): v for k, v in request.headers}
|
||||||
|
|
||||||
|
|
||||||
|
@register_io_handler("request-file-name")
|
||||||
|
async def _io_request_file_name(
|
||||||
|
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||||
|
) -> Any:
|
||||||
|
"""``(request-file-name "field")`` → filename or nil."""
|
||||||
|
if not args:
|
||||||
|
raise ValueError("request-file-name requires a field name")
|
||||||
|
from quart import request
|
||||||
|
from .types import NIL
|
||||||
|
files = await request.files
|
||||||
|
f = files.get(str(args[0]))
|
||||||
|
return f.filename if f else NIL
|
||||||
|
|
||||||
|
|
||||||
|
@register_io_handler("set-response-header")
|
||||||
|
async def _io_set_response_header(
|
||||||
|
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||||
|
) -> Any:
|
||||||
|
"""``(set-response-header "Name" "value")`` → set on response after handler."""
|
||||||
|
if len(args) < 2:
|
||||||
|
raise ValueError("set-response-header requires name and value")
|
||||||
|
from .types import NIL
|
||||||
|
meta = _response_meta.get(None)
|
||||||
|
if meta is None:
|
||||||
|
meta = {"headers": {}, "status": None}
|
||||||
|
_response_meta.set(meta)
|
||||||
|
meta["headers"][str(args[0])] = str(args[1])
|
||||||
|
return NIL
|
||||||
|
|
||||||
|
|
||||||
|
@register_io_handler("set-response-status")
|
||||||
|
async def _io_set_response_status(
|
||||||
|
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||||
|
) -> Any:
|
||||||
|
"""``(set-response-status 503)`` → set status code on response."""
|
||||||
|
if not args:
|
||||||
|
raise ValueError("set-response-status requires a status code")
|
||||||
|
from .types import NIL
|
||||||
|
meta = _response_meta.get(None)
|
||||||
|
if meta is None:
|
||||||
|
meta = {"headers": {}, "status": None}
|
||||||
|
_response_meta.set(meta)
|
||||||
|
meta["status"] = int(args[0])
|
||||||
|
return NIL
|
||||||
|
|
||||||
|
|
||||||
|
@register_io_handler("state-get")
|
||||||
|
async def _io_state_get(
|
||||||
|
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||||
|
) -> Any:
|
||||||
|
"""``(state-get "key" default?)`` → read from ephemeral state."""
|
||||||
|
if not args:
|
||||||
|
raise ValueError("state-get requires a key")
|
||||||
|
from .types import NIL
|
||||||
|
key = str(args[0])
|
||||||
|
default = args[1] if len(args) > 1 else NIL
|
||||||
|
return _ephemeral_state.get(key, default)
|
||||||
|
|
||||||
|
|
||||||
|
@register_io_handler("state-set!")
|
||||||
|
async def _io_state_set(
|
||||||
|
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||||
|
) -> Any:
|
||||||
|
"""``(state-set! "key" value)`` → write to ephemeral state."""
|
||||||
|
if len(args) < 2:
|
||||||
|
raise ValueError("state-set! requires key and value")
|
||||||
|
from .types import NIL
|
||||||
|
_ephemeral_state[str(args[0])] = args[1]
|
||||||
|
return NIL
|
||||||
|
|
||||||
|
|
||||||
@register_io_handler("csrf-token")
|
@register_io_handler("csrf-token")
|
||||||
async def _io_csrf_token(
|
async def _io_csrf_token(
|
||||||
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||||
|
|||||||
@@ -176,6 +176,66 @@
|
|||||||
:doc "Content-Type of the current request."
|
:doc "Content-Type of the current request."
|
||||||
:context :request)
|
:context :request)
|
||||||
|
|
||||||
|
(define-io-primitive "request-args-all"
|
||||||
|
:params ()
|
||||||
|
:returns "dict"
|
||||||
|
:async true
|
||||||
|
:doc "All query string parameters as a dict."
|
||||||
|
:context :request)
|
||||||
|
|
||||||
|
(define-io-primitive "request-form-all"
|
||||||
|
:params ()
|
||||||
|
:returns "dict"
|
||||||
|
:async true
|
||||||
|
:doc "All form fields as a dict."
|
||||||
|
:context :request)
|
||||||
|
|
||||||
|
(define-io-primitive "request-headers-all"
|
||||||
|
:params ()
|
||||||
|
:returns "dict"
|
||||||
|
:async true
|
||||||
|
:doc "All request headers as a dict (lowercase keys)."
|
||||||
|
:context :request)
|
||||||
|
|
||||||
|
(define-io-primitive "request-file-name"
|
||||||
|
:params (field-name)
|
||||||
|
:returns "string?"
|
||||||
|
:async true
|
||||||
|
:doc "Filename of an uploaded file by field name, or nil."
|
||||||
|
:context :request)
|
||||||
|
|
||||||
|
;; Response manipulation
|
||||||
|
|
||||||
|
(define-io-primitive "set-response-header"
|
||||||
|
:params (name value)
|
||||||
|
:returns "nil"
|
||||||
|
:async true
|
||||||
|
:doc "Set a response header. Applied after handler returns."
|
||||||
|
:context :request)
|
||||||
|
|
||||||
|
(define-io-primitive "set-response-status"
|
||||||
|
:params (status)
|
||||||
|
:returns "nil"
|
||||||
|
:async true
|
||||||
|
:doc "Set the HTTP response status code. Applied after handler returns."
|
||||||
|
:context :request)
|
||||||
|
|
||||||
|
;; Ephemeral state — per-process, resets on restart
|
||||||
|
|
||||||
|
(define-io-primitive "state-get"
|
||||||
|
:params (key &rest default)
|
||||||
|
:returns "any"
|
||||||
|
:async true
|
||||||
|
:doc "Read from ephemeral per-process state dict."
|
||||||
|
:context :request)
|
||||||
|
|
||||||
|
(define-io-primitive "state-set!"
|
||||||
|
:params (key value)
|
||||||
|
:returns "nil"
|
||||||
|
:async true
|
||||||
|
:doc "Write to ephemeral per-process state dict."
|
||||||
|
:context :request)
|
||||||
|
|
||||||
|
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
;; Tier 3: Signal primitives — reactive state for islands
|
;; Tier 3: Signal primitives — reactive state for islands
|
||||||
|
|||||||
@@ -38,12 +38,13 @@
|
|||||||
|
|
||||||
|
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
;; defhandler — (defhandler name [:path "..." :method :get :csrf false] (&key param...) body)
|
;; defhandler — (defhandler name [:path "..." :method :get :csrf false :returns "element"] (&key param...) body)
|
||||||
;;
|
;;
|
||||||
;; Keyword options between name and params list:
|
;; Keyword options between name and params list:
|
||||||
;; :path — public route path (string). Without :path, handler is internal-only.
|
;; :path — public route path (string). Without :path, handler is internal-only.
|
||||||
;; :method — HTTP method (keyword: :get :post :put :patch :delete). Default :get.
|
;; :method — HTTP method (keyword: :get :post :put :patch :delete). Default :get.
|
||||||
;; :csrf — CSRF protection (boolean). Default true; set false for POST/PUT etc.
|
;; :csrf — CSRF protection (boolean). Default true; set false for POST/PUT etc.
|
||||||
|
;; :returns — return type annotation (types.sx vocabulary). Default "element".
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
(define parse-handler-args
|
(define parse-handler-args
|
||||||
|
|||||||
@@ -226,10 +226,11 @@ def make_handler_def(name, params, body, env, opts=None):
|
|||||||
path = opts.get('path') if opts else None
|
path = opts.get('path') if opts else None
|
||||||
method = str(opts.get('method', 'get')) if opts else 'get'
|
method = str(opts.get('method', 'get')) if opts else 'get'
|
||||||
csrf = opts.get('csrf', True) if opts else True
|
csrf = opts.get('csrf', True) if opts else True
|
||||||
|
returns = str(opts.get('returns', 'element')) if opts else 'element'
|
||||||
if isinstance(csrf, str):
|
if isinstance(csrf, str):
|
||||||
csrf = csrf.lower() not in ('false', 'nil', 'no')
|
csrf = csrf.lower() not in ('false', 'nil', 'no')
|
||||||
return HandlerDef(name=name, params=list(params), body=body, closure=dict(env),
|
return HandlerDef(name=name, params=list(params), body=body, closure=dict(env),
|
||||||
path=path, method=method.lower(), csrf=csrf)
|
path=path, method=method.lower(), csrf=csrf, returns=returns)
|
||||||
|
|
||||||
|
|
||||||
def make_query_def(name, params, doc, body, env):
|
def make_query_def(name, params, doc, body, env):
|
||||||
|
|||||||
@@ -185,10 +185,11 @@ def make_handler_def(name, params, body, env, opts=None):
|
|||||||
path = opts.get('path') if opts else None
|
path = opts.get('path') if opts else None
|
||||||
method = str(opts.get('method', 'get')) if opts else 'get'
|
method = str(opts.get('method', 'get')) if opts else 'get'
|
||||||
csrf = opts.get('csrf', True) if opts else True
|
csrf = opts.get('csrf', True) if opts else True
|
||||||
|
returns = str(opts.get('returns', 'element')) if opts else 'element'
|
||||||
if isinstance(csrf, str):
|
if isinstance(csrf, str):
|
||||||
csrf = csrf.lower() not in ('false', 'nil', 'no')
|
csrf = csrf.lower() not in ('false', 'nil', 'no')
|
||||||
return HandlerDef(name=name, params=list(params), body=body, closure=dict(env),
|
return HandlerDef(name=name, params=list(params), body=body, closure=dict(env),
|
||||||
path=path, method=method.lower(), csrf=csrf)
|
path=path, method=method.lower(), csrf=csrf, returns=returns)
|
||||||
|
|
||||||
|
|
||||||
def make_query_def(name, params, doc, body, env):
|
def make_query_def(name, params, doc, body, env):
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ class HandlerDef:
|
|||||||
path: str | None = None # public route path (None = internal fragment only)
|
path: str | None = None # public route path (None = internal fragment only)
|
||||||
method: str = "get" # HTTP method (get, post, put, patch, delete)
|
method: str = "get" # HTTP method (get, post, put, patch, delete)
|
||||||
csrf: bool = True # CSRF protection enabled
|
csrf: bool = True # CSRF protection enabled
|
||||||
|
returns: str = "element" # return type (types.sx vocabulary)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_route(self) -> bool:
|
def is_route(self) -> bool:
|
||||||
|
|||||||
@@ -713,84 +713,9 @@ def register(url_prefix: str = "/") -> Blueprint:
|
|||||||
# Reference API endpoints — remaining Python-only
|
# Reference API endpoints — remaining Python-only
|
||||||
#
|
#
|
||||||
# Most reference endpoints migrated to sx/sx/handlers/ref-api.sx.
|
# Most reference endpoints migrated to sx/sx/handlers/ref-api.sx.
|
||||||
# These remain because they need Python-specific features:
|
# SSE stays in Python — fundamentally different paradigm (async generator).
|
||||||
# - File upload access (request.files)
|
|
||||||
# - Dynamic all-params iteration
|
|
||||||
# - Stateful counters with non-200 responses
|
|
||||||
# - SSE streaming
|
|
||||||
# - Custom response headers
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def _ref_wire(wire_id: str, sx_src: str) -> str:
|
|
||||||
"""Build OOB swap showing the wire response text."""
|
|
||||||
from sxc.pages.renders import _oob_code
|
|
||||||
return _oob_code(f"ref-wire-{wire_id}", sx_src)
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
@bp.post("/geography/hypermedia/reference/api/upload-name")
|
|
||||||
async def ref_upload_name():
|
|
||||||
from shared.sx.helpers import sx_response
|
|
||||||
files = await request.files
|
|
||||||
f = files.get("file")
|
|
||||||
name = f.filename if f else "(no file)"
|
|
||||||
sx_src = f'(span :class "text-stone-800 text-sm" "Received: " (strong "{name}"))'
|
|
||||||
oob = _ref_wire("sx-encoding", sx_src)
|
|
||||||
return sx_response(f'(<> {sx_src} {oob})')
|
|
||||||
|
|
||||||
@bp.get("/geography/hypermedia/reference/api/echo-headers")
|
|
||||||
async def ref_echo_headers():
|
|
||||||
from shared.sx.helpers import sx_response
|
|
||||||
custom = [(k, v) for k, v in request.headers if k.lower().startswith("x-")]
|
|
||||||
if not custom:
|
|
||||||
sx_src = '(span :class "text-stone-400 text-sm" "No custom headers received.")'
|
|
||||||
else:
|
|
||||||
items = " ".join(
|
|
||||||
f'(li (strong "{k}") ": " "{v}")' for k, v in custom)
|
|
||||||
sx_src = f'(ul :class "text-sm text-stone-700 space-y-1" {items})'
|
|
||||||
oob = _ref_wire("sx-headers", sx_src)
|
|
||||||
return sx_response(f'(<> {sx_src} {oob})')
|
|
||||||
|
|
||||||
@bp.get("/geography/hypermedia/reference/api/echo-vals")
|
|
||||||
async def ref_echo_vals_get():
|
|
||||||
from shared.sx.helpers import sx_response
|
|
||||||
vals = list(request.args.items())
|
|
||||||
if not vals:
|
|
||||||
sx_src = '(span :class "text-stone-400 text-sm" "No values received.")'
|
|
||||||
else:
|
|
||||||
items = " ".join(
|
|
||||||
f'(li (strong "{k}") ": " "{v}")' for k, v in vals)
|
|
||||||
sx_src = f'(ul :class "text-sm text-stone-700 space-y-1" {items})'
|
|
||||||
oob_include = _ref_wire("sx-include", sx_src)
|
|
||||||
return sx_response(f'(<> {sx_src} {oob_include})')
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
@bp.post("/geography/hypermedia/reference/api/echo-vals")
|
|
||||||
async def ref_echo_vals_post():
|
|
||||||
from shared.sx.helpers import sx_response
|
|
||||||
form = await request.form
|
|
||||||
vals = list(form.items())
|
|
||||||
if not vals:
|
|
||||||
sx_src = '(span :class "text-stone-400 text-sm" "No values received.")'
|
|
||||||
else:
|
|
||||||
items = " ".join(
|
|
||||||
f'(li (strong "{k}") ": " "{v}")' for k, v in vals)
|
|
||||||
sx_src = f'(ul :class "text-sm text-stone-700 space-y-1" {items})'
|
|
||||||
oob = _ref_wire("sx-vals", sx_src)
|
|
||||||
return sx_response(f'(<> {sx_src} {oob})')
|
|
||||||
|
|
||||||
_ref_flaky = {"n": 0}
|
|
||||||
|
|
||||||
@bp.get("/geography/hypermedia/reference/api/flaky")
|
|
||||||
async def ref_flaky():
|
|
||||||
from shared.sx.helpers import sx_response
|
|
||||||
_ref_flaky["n"] += 1
|
|
||||||
n = _ref_flaky["n"]
|
|
||||||
if n % 3 != 0:
|
|
||||||
return Response("", status=503, content_type="text/plain")
|
|
||||||
sx_src = f'(span :class "text-emerald-700 text-sm" "Success on attempt " "{n}" "!")'
|
|
||||||
oob = _ref_wire("sx-retry", sx_src)
|
|
||||||
return sx_response(f'(<> {sx_src} {oob})')
|
|
||||||
|
|
||||||
@bp.get("/geography/hypermedia/reference/api/sse-time")
|
@bp.get("/geography/hypermedia/reference/api/sse-time")
|
||||||
async def ref_sse_time():
|
async def ref_sse_time():
|
||||||
async def generate():
|
async def generate():
|
||||||
@@ -930,24 +855,4 @@ def register(url_prefix: str = "/") -> Blueprint:
|
|||||||
)
|
)
|
||||||
return sx_response(sx_src)
|
return sx_response(sx_src)
|
||||||
|
|
||||||
# --- Header demos ---
|
|
||||||
|
|
||||||
@bp.get("/geography/hypermedia/reference/api/trigger-event")
|
|
||||||
async def ref_trigger_event():
|
|
||||||
from shared.sx.helpers import sx_response
|
|
||||||
now = datetime.now().strftime("%H:%M:%S")
|
|
||||||
sx_src = f'(span :class "text-stone-800 text-sm" "Loaded at " (strong "{now}") " — check the border!")'
|
|
||||||
resp = sx_response(sx_src)
|
|
||||||
resp.headers["SX-Trigger"] = "showNotice"
|
|
||||||
return resp
|
|
||||||
|
|
||||||
@bp.get("/geography/hypermedia/reference/api/retarget")
|
|
||||||
async def ref_retarget():
|
|
||||||
from shared.sx.helpers import sx_response
|
|
||||||
now = datetime.now().strftime("%H:%M:%S")
|
|
||||||
sx_src = f'(span :class "text-violet-700 text-sm" "Retargeted at " (strong "{now}"))'
|
|
||||||
resp = sx_response(sx_src)
|
|
||||||
resp.headers["SX-Retarget"] = "#ref-hdr-retarget-alt"
|
|
||||||
return resp
|
|
||||||
|
|
||||||
return bp
|
return bp
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
(defhandler ref-time
|
(defhandler ref-time
|
||||||
:path "/geography/hypermedia/reference/api/time"
|
:path "/geography/hypermedia/reference/api/time"
|
||||||
:method :get
|
:method :get
|
||||||
|
:returns "element"
|
||||||
(&key)
|
(&key)
|
||||||
(let ((now (now "%H:%M:%S")))
|
(let ((now (now "%H:%M:%S")))
|
||||||
(<>
|
(<>
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
:path "/geography/hypermedia/reference/api/greet"
|
:path "/geography/hypermedia/reference/api/greet"
|
||||||
:method :post
|
:method :post
|
||||||
:csrf false
|
:csrf false
|
||||||
|
:returns "element"
|
||||||
(&key)
|
(&key)
|
||||||
(let ((name (request-form "name" "stranger")))
|
(let ((name (request-form "name" "stranger")))
|
||||||
(<>
|
(<>
|
||||||
@@ -34,6 +36,7 @@
|
|||||||
:path "/geography/hypermedia/reference/api/status"
|
:path "/geography/hypermedia/reference/api/status"
|
||||||
:method :put
|
:method :put
|
||||||
:csrf false
|
:csrf false
|
||||||
|
:returns "element"
|
||||||
(&key)
|
(&key)
|
||||||
(let ((status (request-form "status" "unknown")))
|
(let ((status (request-form "status" "unknown")))
|
||||||
(<>
|
(<>
|
||||||
@@ -47,6 +50,7 @@
|
|||||||
:path "/geography/hypermedia/reference/api/theme"
|
:path "/geography/hypermedia/reference/api/theme"
|
||||||
:method :patch
|
:method :patch
|
||||||
:csrf false
|
:csrf false
|
||||||
|
:returns "element"
|
||||||
(&key)
|
(&key)
|
||||||
(let ((theme (request-form "theme" "unknown")))
|
(let ((theme (request-form "theme" "unknown")))
|
||||||
(<>
|
(<>
|
||||||
@@ -60,6 +64,7 @@
|
|||||||
:path "/geography/hypermedia/reference/api/item/<item_id>"
|
:path "/geography/hypermedia/reference/api/item/<item_id>"
|
||||||
:method :delete
|
:method :delete
|
||||||
:csrf false
|
:csrf false
|
||||||
|
:returns "element"
|
||||||
(&key)
|
(&key)
|
||||||
(<>
|
(<>
|
||||||
(~doc-oob-code :target-id "ref-wire-sx-delete" :text "\"\"")))
|
(~doc-oob-code :target-id "ref-wire-sx-delete" :text "\"\"")))
|
||||||
@@ -69,6 +74,7 @@
|
|||||||
(defhandler ref-trigger-search
|
(defhandler ref-trigger-search
|
||||||
:path "/geography/hypermedia/reference/api/trigger-search"
|
:path "/geography/hypermedia/reference/api/trigger-search"
|
||||||
:method :get
|
:method :get
|
||||||
|
:returns "element"
|
||||||
(&key)
|
(&key)
|
||||||
(let ((q (request-arg "q" "")))
|
(let ((q (request-arg "q" "")))
|
||||||
(let ((sx-text (if (= q "")
|
(let ((sx-text (if (= q "")
|
||||||
@@ -85,6 +91,7 @@
|
|||||||
(defhandler ref-swap-item
|
(defhandler ref-swap-item
|
||||||
:path "/geography/hypermedia/reference/api/swap-item"
|
:path "/geography/hypermedia/reference/api/swap-item"
|
||||||
:method :get
|
:method :get
|
||||||
|
:returns "element"
|
||||||
(&key)
|
(&key)
|
||||||
(let ((now (now "%H:%M:%S")))
|
(let ((now (now "%H:%M:%S")))
|
||||||
(<>
|
(<>
|
||||||
@@ -97,6 +104,7 @@
|
|||||||
(defhandler ref-oob
|
(defhandler ref-oob
|
||||||
:path "/geography/hypermedia/reference/api/oob"
|
:path "/geography/hypermedia/reference/api/oob"
|
||||||
:method :get
|
:method :get
|
||||||
|
:returns "element"
|
||||||
(&key)
|
(&key)
|
||||||
(let ((now (now "%H:%M:%S")))
|
(let ((now (now "%H:%M:%S")))
|
||||||
(<>
|
(<>
|
||||||
@@ -111,6 +119,7 @@
|
|||||||
(defhandler ref-select-page
|
(defhandler ref-select-page
|
||||||
:path "/geography/hypermedia/reference/api/select-page"
|
:path "/geography/hypermedia/reference/api/select-page"
|
||||||
:method :get
|
:method :get
|
||||||
|
:returns "element"
|
||||||
(&key)
|
(&key)
|
||||||
(let ((now (now "%H:%M:%S")))
|
(let ((now (now "%H:%M:%S")))
|
||||||
(<>
|
(<>
|
||||||
@@ -127,6 +136,7 @@
|
|||||||
(defhandler ref-slow-echo
|
(defhandler ref-slow-echo
|
||||||
:path "/geography/hypermedia/reference/api/slow-echo"
|
:path "/geography/hypermedia/reference/api/slow-echo"
|
||||||
:method :get
|
:method :get
|
||||||
|
:returns "element"
|
||||||
(&key)
|
(&key)
|
||||||
(let ((q (request-arg "q" "")))
|
(let ((q (request-arg "q" "")))
|
||||||
(sleep 800)
|
(sleep 800)
|
||||||
@@ -140,6 +150,7 @@
|
|||||||
(defhandler ref-prompt-echo
|
(defhandler ref-prompt-echo
|
||||||
:path "/geography/hypermedia/reference/api/prompt-echo"
|
:path "/geography/hypermedia/reference/api/prompt-echo"
|
||||||
:method :get
|
:method :get
|
||||||
|
:returns "element"
|
||||||
(&key)
|
(&key)
|
||||||
(let ((name (request-header "SX-Prompt" "anonymous")))
|
(let ((name (request-header "SX-Prompt" "anonymous")))
|
||||||
(<>
|
(<>
|
||||||
@@ -152,5 +163,135 @@
|
|||||||
(defhandler ref-error-500
|
(defhandler ref-error-500
|
||||||
:path "/geography/hypermedia/reference/api/error-500"
|
:path "/geography/hypermedia/reference/api/error-500"
|
||||||
:method :get
|
:method :get
|
||||||
|
:returns "nil"
|
||||||
(&key)
|
(&key)
|
||||||
(abort 500 "Server error"))
|
(abort 500 "Server error"))
|
||||||
|
|
||||||
|
|
||||||
|
;; ==========================================================================
|
||||||
|
;; Remaining reference endpoints — migrated from Python
|
||||||
|
;; ==========================================================================
|
||||||
|
|
||||||
|
;; --- sx-encoding demo: file upload name ---
|
||||||
|
|
||||||
|
(defhandler ref-upload-name
|
||||||
|
:path "/geography/hypermedia/reference/api/upload-name"
|
||||||
|
:method :post
|
||||||
|
:csrf false
|
||||||
|
:returns "element"
|
||||||
|
(&key)
|
||||||
|
(let ((name (request-file-name "file")))
|
||||||
|
(let ((display (if (nil? name) "(no file)" name)))
|
||||||
|
(let ((sx-text (str "(span :class \"text-stone-800 text-sm\" \"Received: \" (strong \"" display "\"))")))
|
||||||
|
(<>
|
||||||
|
(span :class "text-stone-800 text-sm" "Received: " (strong display))
|
||||||
|
(~doc-oob-code :target-id "ref-wire-sx-encoding" :text sx-text))))))
|
||||||
|
|
||||||
|
;; --- sx-headers demo: echo custom headers ---
|
||||||
|
|
||||||
|
(defhandler ref-echo-headers
|
||||||
|
:path "/geography/hypermedia/reference/api/echo-headers"
|
||||||
|
:method :get
|
||||||
|
:returns "element"
|
||||||
|
(&key)
|
||||||
|
(let ((all-headers (into (list) (request-headers-all))))
|
||||||
|
(let ((custom (filter
|
||||||
|
(fn (pair) (starts-with? (first pair) "x-"))
|
||||||
|
all-headers)))
|
||||||
|
(let ((sx-text
|
||||||
|
(if (empty? custom)
|
||||||
|
"(span :class \"text-stone-400 text-sm\" \"No custom headers received.\")"
|
||||||
|
(str "(ul :class \"text-sm text-stone-700 space-y-1\" "
|
||||||
|
(join " " (map (fn (pair) (str "(li (strong \"" (first pair) "\") \": \" \"" (nth pair 1) "\")")) custom))
|
||||||
|
")"))))
|
||||||
|
(<>
|
||||||
|
(if (empty? custom)
|
||||||
|
(span :class "text-stone-400 text-sm" "No custom headers received.")
|
||||||
|
(ul :class "text-sm text-stone-700 space-y-1"
|
||||||
|
(map (fn (pair) (li (strong (first pair)) ": " (nth pair 1))) custom)))
|
||||||
|
(~doc-oob-code :target-id "ref-wire-sx-headers" :text sx-text))))))
|
||||||
|
|
||||||
|
;; --- sx-include demo: echo GET query params ---
|
||||||
|
|
||||||
|
(defhandler ref-echo-vals-get
|
||||||
|
:path "/geography/hypermedia/reference/api/echo-vals"
|
||||||
|
:method :get
|
||||||
|
:returns "element"
|
||||||
|
(&key)
|
||||||
|
(let ((vals (into (list) (request-args-all))))
|
||||||
|
(let ((sx-text
|
||||||
|
(if (empty? vals)
|
||||||
|
"(span :class \"text-stone-400 text-sm\" \"No values received.\")"
|
||||||
|
(str "(ul :class \"text-sm text-stone-700 space-y-1\" "
|
||||||
|
(join " " (map (fn (pair) (str "(li (strong \"" (first pair) "\") \": \" \"" (nth pair 1) "\")")) vals))
|
||||||
|
")"))))
|
||||||
|
(<>
|
||||||
|
(if (empty? vals)
|
||||||
|
(span :class "text-stone-400 text-sm" "No values received.")
|
||||||
|
(ul :class "text-sm text-stone-700 space-y-1"
|
||||||
|
(map (fn (pair) (li (strong (first pair)) ": " (nth pair 1))) vals)))
|
||||||
|
(~doc-oob-code :target-id "ref-wire-sx-include" :text sx-text)))))
|
||||||
|
|
||||||
|
;; --- sx-vals demo: echo POST form values ---
|
||||||
|
|
||||||
|
(defhandler ref-echo-vals-post
|
||||||
|
:path "/geography/hypermedia/reference/api/echo-vals"
|
||||||
|
:method :post
|
||||||
|
:csrf false
|
||||||
|
:returns "element"
|
||||||
|
(&key)
|
||||||
|
(let ((vals (into (list) (request-form-all))))
|
||||||
|
(let ((sx-text
|
||||||
|
(if (empty? vals)
|
||||||
|
"(span :class \"text-stone-400 text-sm\" \"No values received.\")"
|
||||||
|
(str "(ul :class \"text-sm text-stone-700 space-y-1\" "
|
||||||
|
(join " " (map (fn (pair) (str "(li (strong \"" (first pair) "\") \": \" \"" (nth pair 1) "\")")) vals))
|
||||||
|
")"))))
|
||||||
|
(<>
|
||||||
|
(if (empty? vals)
|
||||||
|
(span :class "text-stone-400 text-sm" "No values received.")
|
||||||
|
(ul :class "text-sm text-stone-700 space-y-1"
|
||||||
|
(map (fn (pair) (li (strong (first pair)) ": " (nth pair 1))) vals)))
|
||||||
|
(~doc-oob-code :target-id "ref-wire-sx-vals" :text sx-text)))))
|
||||||
|
|
||||||
|
;; --- sx-retry demo: flaky endpoint (fails 2/3 times) ---
|
||||||
|
|
||||||
|
(defhandler ref-flaky
|
||||||
|
:path "/geography/hypermedia/reference/api/flaky"
|
||||||
|
:method :get
|
||||||
|
:returns "element"
|
||||||
|
(&key)
|
||||||
|
(let ((n (+ (state-get "ref-flaky-n" 0) 1)))
|
||||||
|
(state-set! "ref-flaky-n" n)
|
||||||
|
(if (not (= (mod n 3) 0))
|
||||||
|
(do
|
||||||
|
(set-response-status 503)
|
||||||
|
"")
|
||||||
|
(let ((sx-text (str "(span :class \"text-emerald-700 text-sm\" \"Success on attempt \" \"" n "\" \"!\")")))
|
||||||
|
(<>
|
||||||
|
(span :class "text-emerald-700 text-sm" "Success on attempt " (str n) "!")
|
||||||
|
(~doc-oob-code :target-id "ref-wire-sx-retry" :text sx-text))))))
|
||||||
|
|
||||||
|
;; --- sx-trigger-event demo: response header triggers ---
|
||||||
|
|
||||||
|
(defhandler ref-trigger-event
|
||||||
|
:path "/geography/hypermedia/reference/api/trigger-event"
|
||||||
|
:method :get
|
||||||
|
:returns "element"
|
||||||
|
(&key)
|
||||||
|
(let ((now (now "%H:%M:%S")))
|
||||||
|
(set-response-header "SX-Trigger" "showNotice")
|
||||||
|
(<>
|
||||||
|
(span :class "text-stone-800 text-sm" "Loaded at " (strong now) " — check the border!"))))
|
||||||
|
|
||||||
|
;; --- sx-retarget demo: response header retargets ---
|
||||||
|
|
||||||
|
(defhandler ref-retarget
|
||||||
|
:path "/geography/hypermedia/reference/api/retarget"
|
||||||
|
:method :get
|
||||||
|
:returns "element"
|
||||||
|
(&key)
|
||||||
|
(let ((now (now "%H:%M:%S")))
|
||||||
|
(set-response-header "SX-Retarget" "#ref-hdr-retarget-alt")
|
||||||
|
(<>
|
||||||
|
(span :class "text-violet-700 text-sm" "Retargeted at " (strong now)))))
|
||||||
|
|||||||
Reference in New Issue
Block a user