All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m33s
Add boundary.sx declaring all 34 I/O primitives, 32 page helpers, and 9 allowed boundary types. Runtime validation in boundary.py checks every registration against the spec — undeclared primitives/helpers crash at startup with SX_BOUNDARY_STRICT=1 (now set in both dev and prod). Key changes: - Move 5 I/O-in-disguise primitives (app-url, asset-url, config, jinja-global, relations-from) from primitives.py to primitives_io.py - Remove duplicate url-for/route-prefix from primitives.py (already in IO) - Fix parse-datetime to return ISO string instead of raw datetime - Add datetime→isoformat conversion in _convert_result at the edge - Wrap page helper return values with boundary type validation - Replace all SxExpr(f"...") patterns with sx_call() or _sx_fragment() - Add assert declaration to primitives.sx Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
108 lines
3.5 KiB
Python
108 lines
3.5 KiB
Python
"""
|
|
Parse boundary.sx and primitives.sx to extract declared names.
|
|
|
|
Shared by both bootstrap_py.py and bootstrap_js.py, and used at runtime
|
|
by the validation module.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Any
|
|
|
|
# Allow standalone use (from bootstrappers) or in-project imports
|
|
try:
|
|
from shared.sx.parser import parse_all
|
|
from shared.sx.types import Symbol, Keyword, NIL as SX_NIL
|
|
except ImportError:
|
|
import sys
|
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
|
|
sys.path.insert(0, _PROJECT)
|
|
from shared.sx.parser import parse_all
|
|
from shared.sx.types import Symbol, Keyword, NIL as SX_NIL
|
|
|
|
|
|
def _ref_dir() -> str:
|
|
return os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
|
def _read_file(filename: str) -> str:
|
|
filepath = os.path.join(_ref_dir(), filename)
|
|
with open(filepath, encoding="utf-8") as f:
|
|
return f.read()
|
|
|
|
|
|
def _extract_keyword_arg(expr: list, key: str) -> Any:
|
|
"""Extract :key value from a flat keyword-arg list."""
|
|
for i, item in enumerate(expr):
|
|
if isinstance(item, Keyword) and item.name == key and i + 1 < len(expr):
|
|
return expr[i + 1]
|
|
return None
|
|
|
|
|
|
def parse_primitives_sx() -> frozenset[str]:
|
|
"""Parse primitives.sx and return frozenset of declared pure primitive names."""
|
|
source = _read_file("primitives.sx")
|
|
exprs = parse_all(source)
|
|
names: set[str] = set()
|
|
for expr in exprs:
|
|
if (isinstance(expr, list) and len(expr) >= 2
|
|
and isinstance(expr[0], Symbol)
|
|
and expr[0].name == "define-primitive"):
|
|
name = expr[1]
|
|
if isinstance(name, str):
|
|
names.add(name)
|
|
return frozenset(names)
|
|
|
|
|
|
def parse_boundary_sx() -> tuple[frozenset[str], dict[str, frozenset[str]]]:
|
|
"""Parse boundary.sx and return (io_names, {service: helper_names}).
|
|
|
|
Returns:
|
|
io_names: frozenset of declared I/O primitive names
|
|
helpers: dict mapping service name to frozenset of helper names
|
|
"""
|
|
source = _read_file("boundary.sx")
|
|
exprs = parse_all(source)
|
|
io_names: set[str] = set()
|
|
helpers: dict[str, set[str]] = {}
|
|
|
|
for expr in exprs:
|
|
if not isinstance(expr, list) or not expr:
|
|
continue
|
|
head = expr[0]
|
|
if not isinstance(head, Symbol):
|
|
continue
|
|
|
|
if head.name == "define-io-primitive":
|
|
name = expr[1]
|
|
if isinstance(name, str):
|
|
io_names.add(name)
|
|
|
|
elif head.name == "define-page-helper":
|
|
name = expr[1]
|
|
service = _extract_keyword_arg(expr, "service")
|
|
if isinstance(name, str) and isinstance(service, str):
|
|
helpers.setdefault(service, set()).add(name)
|
|
|
|
frozen_helpers = {svc: frozenset(names) for svc, names in helpers.items()}
|
|
return frozenset(io_names), frozen_helpers
|
|
|
|
|
|
def parse_boundary_types() -> frozenset[str]:
|
|
"""Parse boundary.sx and return the declared boundary type names."""
|
|
source = _read_file("boundary.sx")
|
|
exprs = parse_all(source)
|
|
for expr in exprs:
|
|
if (isinstance(expr, list) and len(expr) >= 2
|
|
and isinstance(expr[0], Symbol)
|
|
and expr[0].name == "define-boundary-types"):
|
|
type_list = expr[1]
|
|
if isinstance(type_list, list):
|
|
# (list "number" "string" ...)
|
|
return frozenset(
|
|
item for item in type_list
|
|
if isinstance(item, str)
|
|
)
|
|
return frozenset()
|