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>
160 lines
5.6 KiB
Python
160 lines
5.6 KiB
Python
"""Tests for the declarative handler system."""
|
|
|
|
import asyncio
|
|
import pytest
|
|
from shared.sx import parse, evaluate
|
|
from shared.sx.types import HandlerDef
|
|
from shared.sx.handlers import (
|
|
register_handler,
|
|
get_handler,
|
|
get_all_handlers,
|
|
clear_handlers,
|
|
)
|
|
from shared.sx.async_eval import async_eval, async_render
|
|
from shared.sx.primitives_io import RequestContext
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Registry
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestHandlerRegistry:
|
|
def setup_method(self):
|
|
clear_handlers()
|
|
|
|
def test_register_and_get(self):
|
|
env = {}
|
|
evaluate(parse("(defhandler test-card (&key slug) slug)"), env)
|
|
handler = env["handler:test-card"]
|
|
register_handler("blog", handler)
|
|
assert get_handler("blog", "test-card") is handler
|
|
|
|
def test_get_nonexistent(self):
|
|
assert get_handler("blog", "nope") is None
|
|
|
|
def test_get_all_handlers(self):
|
|
env = {}
|
|
evaluate(parse("(defhandler h1 (&key a) a)"), env)
|
|
evaluate(parse("(defhandler h2 (&key b) b)"), env)
|
|
register_handler("svc", env["handler:h1"])
|
|
register_handler("svc", env["handler:h2"])
|
|
all_h = get_all_handlers("svc")
|
|
assert "h1" in all_h
|
|
assert "h2" in all_h
|
|
|
|
def test_clear_service(self):
|
|
env = {}
|
|
evaluate(parse("(defhandler h1 (&key a) a)"), env)
|
|
register_handler("svc", env["handler:h1"])
|
|
clear_handlers("svc")
|
|
assert get_handler("svc", "h1") is None
|
|
|
|
def test_clear_all(self):
|
|
env = {}
|
|
evaluate(parse("(defhandler h1 (&key a) a)"), env)
|
|
register_handler("svc1", env["handler:h1"])
|
|
register_handler("svc2", env["handler:h1"])
|
|
clear_handlers()
|
|
assert get_all_handlers("svc1") == {}
|
|
assert get_all_handlers("svc2") == {}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# HandlerDef creation via evaluator
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestHandlerDefCreation:
|
|
def test_basic(self):
|
|
env = {}
|
|
evaluate(parse("(defhandler my-handler (&key id name) (str id name))"), env)
|
|
h = env["handler:my-handler"]
|
|
assert isinstance(h, HandlerDef)
|
|
assert h.name == "my-handler"
|
|
assert h.params == ["id", "name"]
|
|
|
|
def test_no_params(self):
|
|
env = {}
|
|
evaluate(parse("(defhandler simple (&key) 42)"), env)
|
|
h = env["handler:simple"]
|
|
assert h.params == []
|
|
|
|
def test_handler_closure_captures_env(self):
|
|
env = {"x": 99}
|
|
evaluate(parse("(defhandler uses-closure (&key) x)"), env)
|
|
h = env["handler:uses-closure"]
|
|
assert h.closure.get("x") == 99
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Async evaluator
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestAsyncEval:
|
|
def test_literals(self):
|
|
ctx = RequestContext()
|
|
assert asyncio.get_event_loop().run_until_complete(
|
|
async_eval(parse("42"), {}, ctx)) == 42
|
|
|
|
def test_let_and_arithmetic(self):
|
|
ctx = RequestContext()
|
|
result = asyncio.get_event_loop().run_until_complete(
|
|
async_eval(parse("(let ((x 10) (y 20)) (+ x y))"), {}, ctx))
|
|
assert result == 30
|
|
|
|
def test_if_when(self):
|
|
ctx = RequestContext()
|
|
result = asyncio.get_event_loop().run_until_complete(
|
|
async_eval(parse("(if true 1 2)"), {}, ctx))
|
|
assert result == 1
|
|
|
|
def test_map_lambda(self):
|
|
ctx = RequestContext()
|
|
result = asyncio.get_event_loop().run_until_complete(
|
|
async_eval(parse("(map (fn (x) (* x x)) (list 1 2 3))"), {}, ctx))
|
|
assert result == [1, 4, 9]
|
|
|
|
def test_macro_expansion(self):
|
|
ctx = RequestContext()
|
|
env = {}
|
|
asyncio.get_event_loop().run_until_complete(
|
|
async_eval(parse("(defmacro double (x) `(+ ,x ,x))"), env, ctx))
|
|
result = asyncio.get_event_loop().run_until_complete(
|
|
async_eval(parse("(double 5)"), env, ctx))
|
|
assert result == 10
|
|
|
|
|
|
class TestAsyncRender:
|
|
def test_simple_html(self):
|
|
ctx = RequestContext()
|
|
result = asyncio.get_event_loop().run_until_complete(
|
|
async_render(parse('(div :class "test" "hello")'), {}, ctx))
|
|
assert result == '<div class="test">hello</div>'
|
|
|
|
def test_component(self):
|
|
ctx = RequestContext()
|
|
env = {}
|
|
evaluate(parse('(defcomp ~bold (&key text) (strong text))'), env)
|
|
result = asyncio.get_event_loop().run_until_complete(
|
|
async_render(parse('(~bold :text "hi")'), env, ctx))
|
|
assert result == "<strong>hi</strong>"
|
|
|
|
def test_let_with_render(self):
|
|
ctx = RequestContext()
|
|
result = asyncio.get_event_loop().run_until_complete(
|
|
async_render(parse('(let ((x "hello")) (span x))'), {}, ctx))
|
|
assert result == "<span>hello</span>"
|
|
|
|
def test_map_render(self):
|
|
ctx = RequestContext()
|
|
result = asyncio.get_event_loop().run_until_complete(
|
|
async_render(parse('(ul (map (fn (x) (li x)) (list "a" "b")))'), {}, ctx))
|
|
assert result == "<ul><li>a</li><li>b</li></ul>"
|
|
|
|
def test_macro_in_render(self):
|
|
ctx = RequestContext()
|
|
env = {}
|
|
evaluate(parse('(defmacro em-text (t) `(em ,t))'), env)
|
|
result = asyncio.get_event_loop().run_until_complete(
|
|
async_render(parse('(em-text "wow")'), env, ctx))
|
|
assert result == "<em>wow</em>"
|