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:
103
protocol/mizan-codegen/tests/ir_deserialization.rs
Normal file
103
protocol/mizan-codegen/tests/ir_deserialization.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
//! IR deserialization tests against the AFI fixture schema.
|
||||
//!
|
||||
//! The fixture is captured from the FastAPI backend's `build_schema()`
|
||||
//! against `tests/afi/fixture.py`. Each test exercises a different facet
|
||||
//! of the IR — function set, per-function field decoding, context-param
|
||||
//! elevation, and components.schemas presence — to confirm the typed
|
||||
//! Rust structs match the JSON shape the backends emit.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use mizan_codegen::fetch::parse_ir_from_str;
|
||||
use mizan_codegen::ir::{AffectKind, IsContext, Transport};
|
||||
|
||||
|
||||
fn load_fixture() -> mizan_codegen::ir::MizanIR {
|
||||
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/fixtures/afi_schema.json");
|
||||
let raw = std::fs::read_to_string(&path)
|
||||
.unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
|
||||
parse_ir_from_str(&raw).unwrap_or_else(|e| panic!("parse IR: {e}"))
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn afi_fixture_deserializes_function_set() {
|
||||
let ir = load_fixture();
|
||||
let names: Vec<&str> = ir.functions.iter().map(|f| f.name.as_str()).collect();
|
||||
|
||||
// Seven fixture functions per tests/afi/fixture.py.
|
||||
assert_eq!(ir.functions.len(), 7, "expected 7 functions, got {}: {names:?}", ir.functions.len());
|
||||
|
||||
for expected in [
|
||||
"echo", "whoami",
|
||||
"user_profile", "user_orders",
|
||||
"update_profile", "find_user", "rename_user",
|
||||
] {
|
||||
assert!(names.contains(&expected), "missing function {expected:?} in {names:?}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn afi_fixture_function_field_decode() {
|
||||
let ir = load_fixture();
|
||||
let echo = ir.functions.iter().find(|f| f.name == "echo").unwrap();
|
||||
assert_eq!(echo.camel_name, "echo");
|
||||
assert!(echo.has_input);
|
||||
assert_eq!(echo.input_type.as_deref(), Some("echoInput"));
|
||||
assert_eq!(echo.output_type, "echoOutput");
|
||||
assert!(!echo.output_nullable);
|
||||
assert_eq!(echo.transport, Transport::Http);
|
||||
assert_eq!(echo.is_context, IsContext::No);
|
||||
|
||||
let whoami = ir.functions.iter().find(|f| f.name == "whoami").unwrap();
|
||||
assert!(!whoami.has_input);
|
||||
|
||||
// `find_user` returns `ProfileOutput | None` — outputNullable must be true.
|
||||
let find_user = ir.functions.iter().find(|f| f.name == "find_user").unwrap();
|
||||
assert!(find_user.output_nullable, "find_user must be outputNullable");
|
||||
|
||||
// Context-typed function picks up the context name.
|
||||
let user_profile = ir.functions.iter().find(|f| f.name == "user_profile").unwrap();
|
||||
assert_eq!(user_profile.is_context.as_str(), Some("user"));
|
||||
|
||||
// Mutation with `affects="user"` lands in `affects` as a context target.
|
||||
let update_profile = ir.functions.iter().find(|f| f.name == "update_profile").unwrap();
|
||||
assert_eq!(update_profile.affects.len(), 1);
|
||||
assert_eq!(update_profile.affects[0].kind, AffectKind::Context);
|
||||
assert_eq!(update_profile.affects[0].name, "user");
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn afi_fixture_context_param_elevation() {
|
||||
let ir = load_fixture();
|
||||
let user = ir.contexts.get("user").expect("user context group");
|
||||
|
||||
// Both context functions share `user_id` as a required param.
|
||||
let user_id = user.params.get("user_id").expect("user_id param");
|
||||
assert_eq!(user_id.ty, "integer");
|
||||
assert!(user_id.required, "user_id is required (declared by every fn in the group)");
|
||||
assert!(user_id.shared_by.contains(&"user_profile".to_string()));
|
||||
assert!(user_id.shared_by.contains(&"user_orders".to_string()));
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn afi_fixture_components_schemas_present() {
|
||||
let ir = load_fixture();
|
||||
// Each fixture function pairs with an *Input/Output schema in components.
|
||||
for expected in [
|
||||
"echoInput", "echoOutput",
|
||||
"whoamiOutput",
|
||||
"userProfileInput", "userProfileOutput",
|
||||
"updateProfileInput", "updateProfileOutput",
|
||||
"findUserInput", "findUserOutput",
|
||||
] {
|
||||
assert!(
|
||||
ir.components.schemas.contains_key(expected),
|
||||
"missing schema {expected:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user