Mizan codegen substrate: Rust kernel + Rust codegen binary, JS generator deleted
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>
This commit is contained in:
@@ -17,9 +17,10 @@ from typing import Any
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
from pydantic import BaseModel, create_model
|
||||
from pydantic import BaseModel, RootModel, create_model
|
||||
|
||||
from mizan_core.registry import get_all_functions, get_context_groups, get_function
|
||||
from mizan_core.type_utils import extract_list_element, extract_optional
|
||||
|
||||
|
||||
__all__ = ["build_schema", "snake_to_camel"]
|
||||
@@ -62,12 +63,20 @@ def _function_metadata(name: str, fn_class: Any) -> dict[str, Any]:
|
||||
input_cls = getattr(fn_class, "Input", None)
|
||||
has_input = _has_input(input_cls)
|
||||
|
||||
output_cls = getattr(fn_class, "Output", None)
|
||||
_, output_nullable = extract_optional(output_cls) if output_cls is not None else (None, False)
|
||||
|
||||
entry: dict[str, Any] = {
|
||||
"name": name,
|
||||
"camelName": camel,
|
||||
"hasInput": has_input,
|
||||
"inputType": f"{camel}Input" if has_input else None,
|
||||
"outputType": f"{camel}Output",
|
||||
# Nullability of the response model — Pydantic `T | None` returns. Carried
|
||||
# on the function entry rather than the schema class because OpenAPI emits
|
||||
# `anyOf: [{$ref}, {type:null}]` at the response level, which strict
|
||||
# deserializers (Rust serde) won't decode as Option<T> without this hint.
|
||||
"outputNullable": output_nullable,
|
||||
"transport": "websocket" if meta.get("websocket") else "http",
|
||||
"isContext": meta.get("context", False),
|
||||
# Form metadata — always emitted so the schema shape matches Django's,
|
||||
@@ -79,6 +88,8 @@ def _function_metadata(name: str, fn_class: Any) -> dict[str, Any]:
|
||||
|
||||
if meta.get("affects"):
|
||||
entry["affects"] = meta["affects"]
|
||||
if meta.get("merge"):
|
||||
entry["merge"] = meta["merge"]
|
||||
|
||||
return entry
|
||||
|
||||
@@ -154,13 +165,28 @@ def build_schema() -> dict[str, Any]:
|
||||
input_type_name = f"{camel}Input" if has_input else None
|
||||
output_type_name = f"{camel}Output"
|
||||
|
||||
# Strip Optional so the rename gets a concrete base — nullability is
|
||||
# carried on the response declaration, not the schema class itself.
|
||||
output_inner, output_nullable = extract_optional(output_cls)
|
||||
|
||||
if has_input:
|
||||
schema_classes[input_type_name] = create_model(
|
||||
input_type_name, __base__=input_cls,
|
||||
)
|
||||
schema_classes[output_type_name] = create_model(
|
||||
output_type_name, __base__=output_cls,
|
||||
)
|
||||
if extract_list_element(output_inner) is not None:
|
||||
# list[T] — RootModel makes the rename emit `type: array` rather
|
||||
# than wrapping the list in a property.
|
||||
schema_classes[output_type_name] = type(
|
||||
output_type_name, (RootModel[output_inner],), {},
|
||||
)
|
||||
else:
|
||||
schema_classes[output_type_name] = create_model(
|
||||
output_type_name, __base__=output_inner,
|
||||
)
|
||||
|
||||
response_model = schema_classes[output_type_name]
|
||||
if output_nullable:
|
||||
response_model = response_model | None
|
||||
|
||||
# Stub endpoint — only exists so FastAPI walks Pydantic types into
|
||||
# components.schemas. Never invoked. Annotations are set explicitly
|
||||
@@ -177,7 +203,7 @@ def build_schema() -> dict[str, Any]:
|
||||
|
||||
schema_app.post(
|
||||
f"/mizan/{name}",
|
||||
response_model=schema_classes[output_type_name],
|
||||
response_model=response_model,
|
||||
operation_id=camel,
|
||||
summary=fn_class.__doc__ or f"Call {name}",
|
||||
)(stub)
|
||||
|
||||
Reference in New Issue
Block a user