Implement flexible entity relation system (Phases A–E)
Declarative relation registry via defrelation s-expressions with cardinality enforcement (one-to-one, one-to-many, many-to-many), registry-aware relate/unrelate/can-relate API endpoints, generic container-nav fragment, and relation-driven UI components. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ Special forms:
|
||||
(lambda (params...) body) or (fn (params...) body)
|
||||
(define name value)
|
||||
(defcomp ~name (&key param...) body)
|
||||
(defrelation :name :from "type" :to "type" :cardinality :card ...)
|
||||
(begin expr...)
|
||||
(quote expr)
|
||||
(do expr...) — alias for begin
|
||||
@@ -32,7 +33,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from .types import Component, Keyword, Lambda, NIL, Symbol
|
||||
from .types import Component, Keyword, Lambda, NIL, RelationDef, Symbol
|
||||
from .primitives import _PRIMITIVES
|
||||
|
||||
|
||||
@@ -429,6 +430,75 @@ def _sf_set_bang(expr: list, env: dict) -> Any:
|
||||
return value
|
||||
|
||||
|
||||
_VALID_CARDINALITIES = {"one-to-one", "one-to-many", "many-to-many"}
|
||||
_VALID_NAV = {"submenu", "tab", "badge", "inline", "hidden"}
|
||||
|
||||
|
||||
def _sf_defrelation(expr: list, env: dict) -> RelationDef:
|
||||
"""``(defrelation :name :from "t" :to "t" :cardinality :card ...)``"""
|
||||
if len(expr) < 2:
|
||||
raise EvalError("defrelation requires a name")
|
||||
|
||||
name_kw = expr[1]
|
||||
if not isinstance(name_kw, Keyword):
|
||||
raise EvalError(f"defrelation name must be a keyword, got {type(name_kw).__name__}")
|
||||
rel_name = name_kw.name
|
||||
|
||||
# Parse keyword args from remaining elements
|
||||
kwargs: dict[str, str | None] = {}
|
||||
i = 2
|
||||
while i < len(expr):
|
||||
key = expr[i]
|
||||
if isinstance(key, Keyword):
|
||||
if i + 1 < len(expr):
|
||||
val = expr[i + 1]
|
||||
if isinstance(val, Keyword):
|
||||
kwargs[key.name] = val.name
|
||||
else:
|
||||
kwargs[key.name] = _eval(val, env) if not isinstance(val, str) else val
|
||||
i += 2
|
||||
else:
|
||||
kwargs[key.name] = None
|
||||
i += 1
|
||||
else:
|
||||
i += 1
|
||||
|
||||
for field in ("from", "to", "cardinality"):
|
||||
if field not in kwargs:
|
||||
raise EvalError(f"defrelation {rel_name} missing required :{field}")
|
||||
|
||||
card = kwargs["cardinality"]
|
||||
if card not in _VALID_CARDINALITIES:
|
||||
raise EvalError(
|
||||
f"defrelation {rel_name}: invalid cardinality {card!r}, "
|
||||
f"expected one of {_VALID_CARDINALITIES}"
|
||||
)
|
||||
|
||||
nav = kwargs.get("nav", "hidden")
|
||||
if nav not in _VALID_NAV:
|
||||
raise EvalError(
|
||||
f"defrelation {rel_name}: invalid nav {nav!r}, "
|
||||
f"expected one of {_VALID_NAV}"
|
||||
)
|
||||
|
||||
defn = RelationDef(
|
||||
name=rel_name,
|
||||
from_type=kwargs["from"],
|
||||
to_type=kwargs["to"],
|
||||
cardinality=card,
|
||||
inverse=kwargs.get("inverse"),
|
||||
nav=nav,
|
||||
nav_icon=kwargs.get("nav-icon"),
|
||||
nav_label=kwargs.get("nav-label"),
|
||||
)
|
||||
|
||||
from .relations import register_relation
|
||||
register_relation(defn)
|
||||
|
||||
env[f"relation:{rel_name}"] = defn
|
||||
return defn
|
||||
|
||||
|
||||
_SPECIAL_FORMS: dict[str, Any] = {
|
||||
"if": _sf_if,
|
||||
"when": _sf_when,
|
||||
@@ -442,6 +512,7 @@ _SPECIAL_FORMS: dict[str, Any] = {
|
||||
"fn": _sf_lambda,
|
||||
"define": _sf_define,
|
||||
"defcomp": _sf_defcomp,
|
||||
"defrelation": _sf_defrelation,
|
||||
"begin": _sf_begin,
|
||||
"do": _sf_begin,
|
||||
"quote": _sf_quote,
|
||||
|
||||
Reference in New Issue
Block a user