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.8 KiB
Rust
104 lines
3.8 KiB
Rust
//! 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:?}",
|
|
);
|
|
}
|
|
}
|