Files
rose-ash/shared/sexp/relations.py
giles a0a0f5ebc2 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>
2026-02-28 08:35:17 +00:00

102 lines
2.7 KiB
Python

"""
Relation registry — declarative entity relationship definitions.
Relations are defined as s-expressions using ``defrelation`` and stored
in a global registry. All services load the same definitions at startup
via ``load_relation_registry()``.
"""
from __future__ import annotations
from shared.sexp.types import RelationDef
# ---------------------------------------------------------------------------
# Registry
# ---------------------------------------------------------------------------
_RELATION_REGISTRY: dict[str, RelationDef] = {}
def register_relation(defn: RelationDef) -> None:
"""Add a RelationDef to the global registry."""
_RELATION_REGISTRY[defn.name] = defn
def get_relation(name: str) -> RelationDef | None:
"""Look up a relation by name (e.g. ``"page->market"``)."""
return _RELATION_REGISTRY.get(name)
def relations_from(entity_type: str) -> list[RelationDef]:
"""All relations where *entity_type* is the ``from`` side."""
return [d for d in _RELATION_REGISTRY.values() if d.from_type == entity_type]
def relations_to(entity_type: str) -> list[RelationDef]:
"""All relations where *entity_type* is the ``to`` side."""
return [d for d in _RELATION_REGISTRY.values() if d.to_type == entity_type]
def all_relations() -> list[RelationDef]:
"""Return all registered relations."""
return list(_RELATION_REGISTRY.values())
def clear_registry() -> None:
"""Clear all registered relations (for testing)."""
_RELATION_REGISTRY.clear()
# ---------------------------------------------------------------------------
# Built-in relation definitions (s-expression source)
# ---------------------------------------------------------------------------
_BUILTIN_RELATIONS = '''
(begin
(defrelation :page->market
:from "page"
:to "market"
:cardinality :one-to-many
:inverse :market->page
:nav :submenu
:nav-icon "fa fa-shopping-bag"
:nav-label "markets")
(defrelation :page->calendar
:from "page"
:to "calendar"
:cardinality :one-to-many
:inverse :calendar->page
:nav :submenu
:nav-icon "fa fa-calendar"
:nav-label "calendars")
(defrelation :post->calendar_entry
:from "post"
:to "calendar_entry"
:cardinality :many-to-many
:inverse :calendar_entry->post
:nav :inline
:nav-icon "fa fa-file-alt"
:nav-label "events")
(defrelation :page->menu_node
:from "page"
:to "menu_node"
:cardinality :one-to-one
:nav :hidden)
)
'''
def load_relation_registry() -> None:
"""Parse built-in defrelation s-expressions and populate the registry."""
from shared.sexp.evaluator import evaluate
from shared.sexp.parser import parse
tree = parse(_BUILTIN_RELATIONS)
evaluate(tree)