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:
121
frontends/mizan-rust/src/error.rs
Normal file
121
frontends/mizan-rust/src/error.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
//! Wire error envelope. Mirrors `MizanError` in `frontends/mizan-base/src/index.ts`.
|
||||
//!
|
||||
//! Two envelope shapes are tolerated:
|
||||
//!
|
||||
//! - FastAPI: `{"error": {"code": "...", "message": "...", "details": ...}}`
|
||||
//! - Django: `{"error": true, "code": "...", "message": "...", "details": ...}`
|
||||
//!
|
||||
//! When neither shape parses, `code` falls back to `HTTP_<status>` and the
|
||||
//! raw response body is the message.
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MizanError {
|
||||
pub status: u16,
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
pub details: Option<Value>,
|
||||
pub raw_body: String,
|
||||
}
|
||||
|
||||
|
||||
impl MizanError {
|
||||
pub fn from_response(status: u16, body: String) -> Self {
|
||||
let parsed = serde_json::from_str::<Envelope>(&body).ok();
|
||||
let (code, message, details) = match parsed {
|
||||
Some(Envelope::Fastapi { error }) => (
|
||||
error.code.unwrap_or_else(|| format!("HTTP_{status}")),
|
||||
error.message.unwrap_or_else(|| format!("Mizan call failed ({status})")),
|
||||
error.details,
|
||||
),
|
||||
Some(Envelope::Django { code, message, details, .. }) => (
|
||||
code.unwrap_or_else(|| format!("HTTP_{status}")),
|
||||
message.unwrap_or_else(|| format!("Mizan call failed ({status})")),
|
||||
details,
|
||||
),
|
||||
None => (
|
||||
format!("HTTP_{status}"),
|
||||
format!("Mizan call failed ({status})"),
|
||||
None,
|
||||
),
|
||||
};
|
||||
Self { status, code, message, details, raw_body: body }
|
||||
}
|
||||
|
||||
pub fn transport(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
status: 0,
|
||||
code: "TRANSPORT".to_string(),
|
||||
message: message.into(),
|
||||
details: None,
|
||||
raw_body: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl std::fmt::Display for MizanError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Mizan {} ({}): {}", self.status, self.code, self.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl std::error::Error for MizanError {}
|
||||
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum Envelope {
|
||||
Fastapi { error: NestedError },
|
||||
Django {
|
||||
// Django form is `{"error": true, "code": ..., "message": ..., "details": ...}`.
|
||||
// `error` is a bool sentinel; the actual fields are siblings.
|
||||
#[allow(dead_code)]
|
||||
error: bool,
|
||||
code: Option<String>,
|
||||
message: Option<String>,
|
||||
details: Option<Value>,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct NestedError {
|
||||
code: Option<String>,
|
||||
message: Option<String>,
|
||||
details: Option<Value>,
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_fastapi_envelope() {
|
||||
let body = r#"{"error":{"code":"BAD_REQUEST","message":"oops","details":{"k":1}}}"#;
|
||||
let e = MizanError::from_response(400, body.to_string());
|
||||
assert_eq!(e.code, "BAD_REQUEST");
|
||||
assert_eq!(e.message, "oops");
|
||||
assert_eq!(e.details, Some(serde_json::json!({"k": 1})));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_django_envelope() {
|
||||
let body = r#"{"error":true,"code":"NOT_FOUND","message":"missing","details":null}"#;
|
||||
let e = MizanError::from_response(404, body.to_string());
|
||||
assert_eq!(e.code, "NOT_FOUND");
|
||||
assert_eq!(e.message, "missing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn falls_back_on_unparseable_body() {
|
||||
let e = MizanError::from_response(500, "Internal Server Error".to_string());
|
||||
assert_eq!(e.code, "HTTP_500");
|
||||
assert!(e.message.contains("500"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user