Add macros, declarative handlers (defhandler), and convert all fragment routes to sx
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Phase 1 — Macros: defmacro + quasiquote syntax (`, ,, ,@) in parser,
evaluator, HTML renderer, and JS mirror. Macro type, expansion, and
round-trip serialization.
Phase 2 — Expanded primitives: app-url, url-for, asset-url, config,
format-date, parse-int (pure); service, request-arg, request-path,
nav-tree, get-children (I/O); jinja-global, relations-from (pure).
Updated _io_service to accept (service "registry-name" "method" :kwargs)
with auto kebab→snake conversion. DTO-to-dict now expands datetime fields
into year/month/day convenience keys. Tuple returns converted to lists.
Phase 3 — Declarative handlers: HandlerDef type, defhandler special form,
handler registry (service → name → HandlerDef), async evaluator+renderer
(async_eval.py) that awaits I/O primitives inline within control flow.
Handler loading from .sx files, execute_handler, blueprint factory.
Phase 4 — Convert all fragment routes: 13 Python fragment handlers across
8 services replaced with declarative .sx handler files. All routes.py
simplified to uniform sx dispatch pattern. Two Jinja HTML handlers
(events/container-cards, events/account-page) kept as Python.
New files: shared/sx/async_eval.py, shared/sx/handlers.py,
shared/sx/tests/test_handlers.py, plus 13 handler .sx files under
{service}/sx/handlers/. MarketService.product_by_slug() added.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -25,7 +25,7 @@ import hashlib
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from .types import NIL, Component, Keyword, Symbol
|
||||
from .types import NIL, Component, Keyword, Macro, Symbol
|
||||
from .parser import parse
|
||||
from .html import render as html_render, _render_component
|
||||
|
||||
@@ -54,7 +54,7 @@ def get_component_hash() -> str:
|
||||
|
||||
|
||||
def _compute_component_hash() -> None:
|
||||
"""Recompute _COMPONENT_HASH from all registered Component definitions."""
|
||||
"""Recompute _COMPONENT_HASH from all registered Component and Macro definitions."""
|
||||
global _COMPONENT_HASH
|
||||
from .parser import serialize
|
||||
parts = []
|
||||
@@ -67,6 +67,13 @@ def _compute_component_hash() -> None:
|
||||
params_sx = "(" + " ".join(param_strs) + ")"
|
||||
body_sx = serialize(val.body)
|
||||
parts.append(f"(defcomp ~{val.name} {params_sx} {body_sx})")
|
||||
elif isinstance(val, Macro):
|
||||
param_strs = list(val.params)
|
||||
if val.rest_param:
|
||||
param_strs.extend(["&rest", val.rest_param])
|
||||
params_sx = "(" + " ".join(param_strs) + ")"
|
||||
body_sx = serialize(val.body)
|
||||
parts.append(f"(defmacro {val.name} {params_sx} {body_sx})")
|
||||
if parts:
|
||||
digest = hashlib.sha256("\n".join(parts).encode()).hexdigest()[:12]
|
||||
_COMPONENT_HASH = digest
|
||||
@@ -118,13 +125,33 @@ def reload_if_changed() -> None:
|
||||
load_sx_dir(directory)
|
||||
|
||||
|
||||
def load_service_components(service_dir: str) -> None:
|
||||
"""Load service-specific s-expression components from {service_dir}/sx/."""
|
||||
def load_service_components(service_dir: str, service_name: str | None = None) -> None:
|
||||
"""Load service-specific s-expression components and handlers.
|
||||
|
||||
Components from ``{service_dir}/sx/`` and handlers from
|
||||
``{service_dir}/sx/handlers/`` or ``{service_dir}/sx/handlers.sx``.
|
||||
"""
|
||||
sx_dir = os.path.join(service_dir, "sx")
|
||||
if os.path.isdir(sx_dir):
|
||||
load_sx_dir(sx_dir)
|
||||
watch_sx_dir(sx_dir)
|
||||
|
||||
# Load handler definitions if service_name is provided
|
||||
if service_name:
|
||||
load_handler_dir(os.path.join(sx_dir, "handlers"), service_name)
|
||||
# Also check for a single handlers.sx file
|
||||
handlers_file = os.path.join(sx_dir, "handlers.sx")
|
||||
if os.path.isfile(handlers_file):
|
||||
from .handlers import load_handler_file
|
||||
load_handler_file(handlers_file, service_name)
|
||||
|
||||
|
||||
def load_handler_dir(directory: str, service_name: str) -> None:
|
||||
"""Load handler .sx files from a directory if it exists."""
|
||||
if os.path.isdir(directory):
|
||||
from .handlers import load_handler_dir as _load
|
||||
_load(directory, service_name)
|
||||
|
||||
|
||||
def register_components(sx_source: str) -> None:
|
||||
"""Parse and evaluate s-expression component definitions into the
|
||||
@@ -262,17 +289,25 @@ def client_components_tag(*names: str) -> str:
|
||||
from .parser import serialize
|
||||
parts = []
|
||||
for key, val in _COMPONENT_ENV.items():
|
||||
if not isinstance(val, Component):
|
||||
continue
|
||||
if names and val.name not in names and key.lstrip("~") not in names:
|
||||
continue
|
||||
# Reconstruct defcomp source from the Component object
|
||||
param_strs = ["&key"] + list(val.params)
|
||||
if val.has_children:
|
||||
param_strs.extend(["&rest", "children"])
|
||||
params_sx = "(" + " ".join(param_strs) + ")"
|
||||
body_sx = serialize(val.body, pretty=True)
|
||||
parts.append(f"(defcomp ~{val.name} {params_sx} {body_sx})")
|
||||
if isinstance(val, Component):
|
||||
if names and val.name not in names and key.lstrip("~") not in names:
|
||||
continue
|
||||
# Reconstruct defcomp source from the Component object
|
||||
param_strs = ["&key"] + list(val.params)
|
||||
if val.has_children:
|
||||
param_strs.extend(["&rest", "children"])
|
||||
params_sx = "(" + " ".join(param_strs) + ")"
|
||||
body_sx = serialize(val.body, pretty=True)
|
||||
parts.append(f"(defcomp ~{val.name} {params_sx} {body_sx})")
|
||||
elif isinstance(val, Macro):
|
||||
if names and val.name not in names:
|
||||
continue
|
||||
param_strs = list(val.params)
|
||||
if val.rest_param:
|
||||
param_strs.extend(["&rest", val.rest_param])
|
||||
params_sx = "(" + " ".join(param_strs) + ")"
|
||||
body_sx = serialize(val.body, pretty=True)
|
||||
parts.append(f"(defmacro {val.name} {params_sx} {body_sx})")
|
||||
if not parts:
|
||||
return ""
|
||||
source = "\n".join(parts)
|
||||
|
||||
Reference in New Issue
Block a user