Skip to content

Commit 12d4bd9

Browse files
dmealingclaude
andcommitted
feat(python): metaJson helper for the GET /_meta contract
Exports META_ROUTE_PATH ("/_meta") and meta_json(root), a thin wrapper over canonical_serialize_effective. Python ships the helper only, not a mount: neither metaobjects.runtime nor metaobjects.codegen.runtime imports a web framework, and one endpoint is not a reason to make either framework-bound (mirrors the C# and Java ports). Closes a barrel gap: canonical_serialize_effective was not exported from metaobjects/__init__.py — exported it alongside meta_json. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015NrWUxA6euwvnFcwKxcK6A
1 parent d13bfc1 commit 12d4bd9

3 files changed

Lines changed: 105 additions & 0 deletions

File tree

‎server/python/src/metaobjects/__init__.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@
3535
extract_schema_for,
3636
)
3737
from .meta.core.object.value_object import ValueObject
38+
from .meta_endpoint import META_ROUTE_PATH, meta_json
3839
from .naming import resolve_column_name
40+
from .serializer_json import canonical_serialize_effective
3941

4042
# Module-level shortcuts: the 99% case for callers who don't need a
4143
# long-lived loader. Signatures + docstrings come straight from the
@@ -60,6 +62,10 @@
6062
"load_uris",
6163
"load_string",
6264
"resolve_column_name",
65+
# UI-1 — the metadata API contract (GET /_meta)
66+
"META_ROUTE_PATH",
67+
"meta_json",
68+
"canonical_serialize_effective",
6369
# Runtime object model (Phase A)
6470
"ValueObject",
6571
"MetaObjectAware",
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""UI-1 — the metadata API contract.
2+
3+
Python ships the helper and not a mount: neither ``metaobjects.runtime`` nor
4+
``metaobjects.codegen.runtime`` imports a web framework, and one endpoint is not
5+
a reason to make either of them framework-bound. Mount it yourself::
6+
7+
@app.get(f"/api{META_ROUTE_PATH}")
8+
def meta() -> Response:
9+
return Response(meta_json(root), media_type="application/json")
10+
11+
Guard that route with your own authorization — ``/_meta`` publishes the shape of
12+
the model (entity and field names, types, validators, layouts), though no row data.
13+
"""
14+
from __future__ import annotations
15+
16+
from metaobjects.meta.meta_data import MetaData
17+
from metaobjects.serializer_json import canonical_serialize_effective
18+
19+
#: The endpoint path, mounted under the host's API prefix. A cross-port contract
20+
#: value — one browser read-model works against every backend.
21+
META_ROUTE_PATH = "/_meta"
22+
23+
24+
def meta_json(root: MetaData) -> str:
25+
"""The response body: the model as EFFECTIVE canonical JSON.
26+
27+
Effective, not raw: the effective form materializes the super-chain merge, so
28+
a browser reading it never resolves ``extends`` itself.
29+
"""
30+
return canonical_serialize_effective(root)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""UI-1 — GET /_meta contract: the Python `meta_json` helper.
2+
3+
Loads the shared `extends-abstract-base` conformance fixture (real object-level
4+
inheritance: `Subscriber extends acme::BaseEntity`, and `BaseEntity` declares
5+
`createdAt`) and asserts `meta_json` returns the EFFECTIVE canonical
6+
serialization — the one that inlines inherited members — never the raw one.
7+
"""
8+
from __future__ import annotations
9+
10+
from pathlib import Path
11+
12+
from metaobjects import META_ROUTE_PATH, MetaDataLoader, meta_json
13+
from metaobjects.core_types import core_provider
14+
from metaobjects.serializer_json import canonical_serialize, canonical_serialize_effective
15+
16+
17+
def _fixtures_root() -> Path:
18+
"""Walk up from this file to find the repo-shared `fixtures/conformance` dir
19+
(mirrors `tests/conformance/corpus.py`'s `corpus_root()` — no hardcoded path)."""
20+
here = Path(__file__).resolve()
21+
for parent in [here, *here.parents]:
22+
candidate = parent / "fixtures" / "conformance"
23+
if candidate.is_dir():
24+
return candidate
25+
raise RuntimeError("could not locate fixtures/conformance from " + str(here))
26+
27+
28+
def load_extends_fixture():
29+
"""Load `fixtures/conformance/extends-abstract-base/input` and assert a
30+
clean load (ADR-0023: a fixture with no `expected-errors.json` must load
31+
with zero errors)."""
32+
input_dir = _fixtures_root() / "extends-abstract-base" / "input"
33+
result = MetaDataLoader.from_directory(input_dir, providers=[core_provider])
34+
assert not result.errors, result.errors
35+
return result.root
36+
37+
38+
def test_meta_route_path_is_the_cross_port_contract_value() -> None:
39+
assert META_ROUTE_PATH == "/_meta"
40+
41+
42+
def test_meta_json_returns_the_effective_canonical_serialization() -> None:
43+
root = load_extends_fixture()
44+
assert meta_json(root) == canonical_serialize_effective(root)
45+
46+
47+
def test_meta_json_inlines_a_member_inherited_via_extends() -> None:
48+
"""The real discriminator: `createdAt` is declared only on `BaseEntity`, not
49+
on `Subscriber`. `Subscriber` is the last top-level object in the fixture's
50+
document, in both raw and effective form, so slicing from its `"name"` key
51+
to end-of-document isolates exactly its own block (own children only, raw;
52+
the super-chain-merged set, effective) without picking up `BaseEntity`'s
53+
unrelated `createdAt` declaration earlier in the same document.
54+
55+
`createdAt` must appear in that slice for the EFFECTIVE serialization
56+
(proving the super-chain merge ran) and must be ABSENT from that slice for
57+
the RAW one — which is exactly what would fail if `meta_json` ever called
58+
`canonical_serialize` instead of `canonical_serialize_effective`.
59+
"""
60+
root = load_extends_fixture()
61+
62+
effective = meta_json(root)
63+
raw = canonical_serialize(root)
64+
65+
effective_subscriber = effective[effective.index('"name": "Subscriber"'):]
66+
raw_subscriber = raw[raw.index('"name": "Subscriber"'):]
67+
68+
assert "createdAt" in effective_subscriber
69+
assert "createdAt" not in raw_subscriber

0 commit comments

Comments
 (0)