The Mizan codegen substrate moves off JavaScript template-literal emission
onto a compiled Rust binary that consumes the same OpenAPI + x-mizan-* IR
the JS substrate consumed. Three structural wins fall out of one move:
1. Moat closes. The codegen logic (how `affects` becomes auto-invalidation,
how named contexts collapse onto bundled fetches, how the registry-to-
Provider mapping is shaped) ships compiled instead of as source bytes
in every consumer's node_modules.
2. Pattern F (lines.push append-walls) becomes structurally unauthorable.
The emit substrate is askama templates in templates/<target>/*.j2 —
actual target-language files with {{ ... }} substitution markers,
syntax-highlighted natively, type-checked against the render context
structs at compile time. The Rust emit modules build typed render
contexts and call .render(); no string-builder surface exists.
3. OpenAPI `default`-bearing fields now emit as non-optional in TS / Python
/ Rust — the server always populates them, so consumer code reads them
without nullable checks. Surfaced by Blazr's typecheck on regeneration.
Layout:
frontends/mizan-rust/ — Rust port of @mizan/base; #[cfg(feature="pyo3")]
exposes PyMizanClient for the Python target.
protocol/mizan-codegen/ — codegen binary source + askama templates.
protocol/mizan-generate/ — npm-package shim. bin/launcher.mjs dispatches
to the platform-appropriate prebuilt binary.
Old generator/ JS tree deleted.
tests/rust/ — wire-parity drivers. drive_kernel exercises
raw client.call() / fetch_context(); drive_emitted
exercises the typed crate the codegen emits.
tests/afi/afi_codegen_app.py — codegen entrypoint module (imports + registers).
backends/mizan-fastapi/.../schema.py — adds outputNullable so the Rust
codegen can wrap T | None responses in Option<T>.
Verification:
- 20 mizan-codegen tests green (IR deserialization, byte-equivalent
parity vs JS baseline for stage1/rust/python/react/vue/svelte,
structural test for channels).
- tests/rust/run_wire_parity.py — 12/12 probes green via the Rust binary
driving the FastAPI fixture end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
104 lines
3.1 KiB
Python
104 lines
3.1 KiB
Python
"""Drive the wire-parity check end-to-end.
|
|
|
|
1. Boot the FastAPI fixture app via uvicorn on a free port.
|
|
2. Poll /openapi.json until the server is up.
|
|
3. Run the Rust `drive_kernel` binary (raw kernel calls) against it.
|
|
4. Run the Rust `drive_emitted` binary (typed codegen functions) against
|
|
the same server.
|
|
5. Tear the server down.
|
|
|
|
Either non-zero driver exit propagates as the script's exit code.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import urllib.error
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
AFI_DIR = REPO_ROOT / "tests" / "afi"
|
|
RUST_DIR = REPO_ROOT / "tests" / "rust"
|
|
BOOT_TIMEOUT_S = 15.0
|
|
POLL_INTERVAL_S = 0.25
|
|
|
|
|
|
def pick_free_port() -> int:
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.bind(("127.0.0.1", 0))
|
|
return s.getsockname()[1]
|
|
|
|
|
|
def wait_for_server(port: int, timeout_s: float) -> bool:
|
|
deadline = time.monotonic() + timeout_s
|
|
url = f"http://127.0.0.1:{port}/openapi.json"
|
|
while time.monotonic() < deadline:
|
|
try:
|
|
with urllib.request.urlopen(url, timeout=1.0) as resp:
|
|
if resp.status == 200:
|
|
return True
|
|
except (urllib.error.URLError, ConnectionError, OSError) as e:
|
|
# Surface the kind of failure so a stuck boot doesn't read
|
|
# as "silently waiting"; the loop continues until timeout.
|
|
sys.stderr.write(f"[wire_parity] waiting for server: {type(e).__name__}\n")
|
|
time.sleep(POLL_INTERVAL_S)
|
|
return False
|
|
|
|
|
|
def run_driver(name: str, base_url: str) -> int:
|
|
sys.stdout.write(f"\n=== {name} ===\n")
|
|
sys.stdout.flush()
|
|
return subprocess.run(
|
|
["cargo", "run", "--quiet", "--bin", name, "--", base_url],
|
|
cwd=RUST_DIR,
|
|
).returncode
|
|
|
|
|
|
def main() -> int:
|
|
port = pick_free_port()
|
|
base_url = f"http://127.0.0.1:{port}/api/mizan"
|
|
|
|
server = subprocess.Popen(
|
|
["uv", "run", "uvicorn", "fastapi_app:make_app",
|
|
"--factory", "--port", str(port), "--log-level", "warning"],
|
|
cwd=AFI_DIR,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.PIPE,
|
|
)
|
|
|
|
try:
|
|
if not wait_for_server(port, BOOT_TIMEOUT_S):
|
|
sys.stderr.write(
|
|
f"[wire_parity] server failed to start within {BOOT_TIMEOUT_S}s\n",
|
|
)
|
|
stderr_tail = server.stderr.read(4096) if server.stderr else b""
|
|
if stderr_tail:
|
|
sys.stderr.write(stderr_tail.decode("utf-8", errors="replace"))
|
|
return 1
|
|
|
|
failures = 0
|
|
for driver in ("drive_kernel", "drive_emitted"):
|
|
rc = run_driver(driver, base_url)
|
|
if rc != 0:
|
|
sys.stderr.write(f"[wire_parity] {driver} exited {rc}\n")
|
|
failures += 1
|
|
|
|
return 0 if failures == 0 else 1
|
|
finally:
|
|
server.terminate()
|
|
try:
|
|
server.wait(timeout=3)
|
|
except subprocess.TimeoutExpired:
|
|
server.kill()
|
|
server.wait()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|