Mizan IR: cut over to KDL, delete OpenAPI envelope
Replaces the transitional OpenAPI 3.0 + `x-mizan-*` extensions
substrate with the canonical Mizan IR as KDL, per docs/AFI_ARCHITECTURE.md:
"KDL is the contract; everything else (REST envelopes, OpenAPI
documents, framework idioms) is sediment around it."
End-to-end cutover. No transitional path left on main.
Forward direction:
cores/mizan-python/src/mizan_core/ir.py
build_ir() walks mizan_core.registry, introspects Pydantic
models directly (no JSON-Schema indirection), and emits the
Mizan IR document. The KDL grammar is locked in this file's
module docstring.
Backends emit KDL:
backends/mizan-fastapi/src/mizan_fastapi/ir.py
`python -m mizan_fastapi.ir <module>` — CLI entry point.
backends/mizan-django/.../management/commands/export_mizan_ir.py
`manage.py export_mizan_ir` — Django mgmt command.
Codegen consumes KDL:
protocol/mizan-codegen/Cargo.toml: + kdl = "6"
protocol/mizan-codegen/src/ir.rs: NamedType { Struct/List/Enum/Alias }
+ TypeShape { Primitive/Ref/List/Optional/Enum/Union } sum types,
replacing the JsonSchema sprawl. KDL parser walks the
`kdl::KdlDocument` tree into typed Rust structs.
protocol/mizan-codegen/src/fetch.rs: subprocess command switches
to the new IR-export entry points.
All emit modules (stage1 / react / python / rust / vue / svelte /
channels) port their type-walkers from JsonSchema to the new
sum types — case analysis collapses substantially.
Substrate-honesty wins beyond the moat closure:
- `int | bool` multi-arm unions land as `TypeShape::Union` (was
silently coerced to "string" before).
- `<CamelName>Output = list[T]` returns emit as named alias
types instead of struct-shaped wrappers, so consumer code
`.map()` works directly on the type.
- Pydantic field defaults flow through to `default` properties
in KDL, then back to non-optional shape in every target.
Deleted:
- backends/mizan-fastapi/src/mizan_fastapi/{cli,schema}.py
- backends/mizan-django/.../export_mizan_schema.py
- openapi-bearing half of mizan/export/__init__.py (edge
manifest generator preserved — separate concern).
- tests/afi/schema_normalizer.py
- tests/fixtures/{afi_schema.json, channels_schema.json}
- tests/fixtures/js_* baseline directories.
Verification:
- 20 mizan-codegen unit tests green (IR deserialization,
byte-equivalence parity across stage1/rust/python/react/vue/svelte
against fresh KDL-driven baselines, channels structural).
- tests/rust/run_wire_parity.py: 12/12 probes green driving
the binary end-to-end through KDL.
- Blazr studio-ui typechecks against the regenerated React client.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,10 @@ use crate::config::{Config, RustKernelSpec};
|
||||
use crate::emit::CodegenTarget;
|
||||
use crate::emit::EmittedFile;
|
||||
use crate::emit::casing::{pascal_case, rust_ident, rust_type_ident, snake_case};
|
||||
use crate::ir::{IsContext, JsonSchema, MizanContext, MizanFunction, MizanIR};
|
||||
use crate::ir::{
|
||||
IsContext, MizanContext, MizanFunction, MizanIR, NamedType, Primitive,
|
||||
StructField as IrStructField, TypeShape,
|
||||
};
|
||||
|
||||
|
||||
pub struct RustCrate;
|
||||
@@ -35,7 +38,7 @@ impl CodegenTarget for RustCrate {
|
||||
.render().expect("Cargo.toml renders"),
|
||||
));
|
||||
|
||||
out.push(EmittedFile::new("src/types.rs", emit_types_rs(&ir.components.schemas)));
|
||||
out.push(EmittedFile::new("src/types.rs", emit_types_rs(&ir.types)));
|
||||
|
||||
let mut context_modules: Vec<String> = Vec::new();
|
||||
for (ctx_name, ctx_meta) in &ir.contexts {
|
||||
@@ -119,8 +122,8 @@ struct ContextTemplate<'a> {
|
||||
snake: String,
|
||||
ctx_name: &'a str,
|
||||
type_imports: Vec<String>,
|
||||
data_fields: Vec<StructField>,
|
||||
params: Vec<StructField>,
|
||||
data_fields: Vec<RustField>,
|
||||
params: Vec<RustField>,
|
||||
}
|
||||
|
||||
|
||||
@@ -144,7 +147,10 @@ struct TypesTemplate {
|
||||
}
|
||||
|
||||
|
||||
struct StructField {
|
||||
/// Renderer-side view of a single Rust struct field. Distinct from
|
||||
/// `ir::StructField` (the IR shape) because the renderer carries
|
||||
/// already-rendered identifiers and rename flags.
|
||||
struct RustField {
|
||||
raw_name: String,
|
||||
ident: String,
|
||||
ty: String,
|
||||
@@ -212,10 +218,10 @@ fn emit_context_file(
|
||||
ctx_fns.iter().map(|f| rust_type_ident(&f.output_type)),
|
||||
);
|
||||
|
||||
let data_fields: Vec<StructField> = ctx_fns.iter()
|
||||
let data_fields: Vec<RustField> = ctx_fns.iter()
|
||||
.map(|f| {
|
||||
let ident = rust_ident(&f.name);
|
||||
StructField {
|
||||
RustField {
|
||||
has_rename: ident != f.name,
|
||||
raw_name: f.name.clone(),
|
||||
ident,
|
||||
@@ -224,12 +230,12 @@ fn emit_context_file(
|
||||
})
|
||||
.collect();
|
||||
|
||||
let params: Vec<StructField> = ctx_meta.params.iter()
|
||||
let params: Vec<RustField> = ctx_meta.params.iter()
|
||||
.map(|(p_name, p_meta)| {
|
||||
let ident = rust_ident(p_name);
|
||||
let base = param_rust_type(&p_meta.ty);
|
||||
let base = param_rust_type(p_meta.ty);
|
||||
let ty = if p_meta.required { base.to_string() } else { format!("Option<{base}>") };
|
||||
StructField {
|
||||
RustField {
|
||||
has_rename: ident != *p_name,
|
||||
raw_name: p_name.clone(),
|
||||
ident,
|
||||
@@ -249,12 +255,12 @@ fn emit_context_file(
|
||||
}
|
||||
|
||||
|
||||
fn param_rust_type(json_ty: &str) -> &'static str {
|
||||
match json_ty {
|
||||
"integer" => "i64",
|
||||
"number" => "f64",
|
||||
"boolean" => "bool",
|
||||
_ => "String",
|
||||
fn param_rust_type(p: Primitive) -> &'static str {
|
||||
match p {
|
||||
Primitive::Integer => "i64",
|
||||
Primitive::Number => "f64",
|
||||
Primitive::Boolean => "bool",
|
||||
Primitive::String => "String",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,33 +309,26 @@ fn emit_call_file(fn_meta: &MizanFunction) -> String {
|
||||
// ─── types.rs ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
/// Per-types-file context tracking enum names hoisted out of inline
|
||||
/// `field { enum "a" "b" }` declarations into Rust top-level enum types.
|
||||
struct EnumCtx {
|
||||
hoisted: Vec<(String, Vec<serde_json::Value>)>,
|
||||
depth: usize,
|
||||
hoisted: Vec<(String, Vec<String>)>,
|
||||
enum_name: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
fn emit_types_rs(schemas: &IndexMap<String, JsonSchema>) -> String {
|
||||
let mut ctx = EnumCtx { hoisted: Vec::new(), depth: 0, enum_name: None };
|
||||
fn emit_types_rs(types: &IndexMap<String, NamedType>) -> String {
|
||||
let mut ctx = EnumCtx { hoisted: Vec::new(), enum_name: None };
|
||||
|
||||
let schemas_block = schemas.iter()
|
||||
.map(|(raw_name, schema)| {
|
||||
let schemas_block = types.iter()
|
||||
.map(|(raw_name, ty)| {
|
||||
let name = rust_type_ident(raw_name);
|
||||
if let Some(values) = &schema.r#enum {
|
||||
if schema.ty.as_deref() == Some("string") {
|
||||
return emit_string_enum(&name, values);
|
||||
}
|
||||
match ty {
|
||||
NamedType::Struct(fields) => emit_struct_decl(&name, fields, &mut ctx),
|
||||
NamedType::List(inner) => emit_transparent_array(&name, inner, &mut ctx),
|
||||
NamedType::Enum(variants) => emit_string_enum(&name, variants),
|
||||
NamedType::Alias(inner) => emit_type_alias(&name, inner, &mut ctx),
|
||||
}
|
||||
if schema.ty.as_deref() == Some("array") {
|
||||
return emit_transparent_array(&name, schema, &mut ctx);
|
||||
}
|
||||
if schema.ty.as_deref() == Some("object") {
|
||||
if let Some(props) = &schema.properties {
|
||||
return emit_struct(&name, schema, props, &mut ctx);
|
||||
}
|
||||
}
|
||||
emit_type_alias(&name, schema, &mut ctx)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
@@ -344,12 +343,11 @@ fn emit_types_rs(schemas: &IndexMap<String, JsonSchema>) -> String {
|
||||
}
|
||||
|
||||
|
||||
fn emit_string_enum(name: &str, variants: &[serde_json::Value]) -> String {
|
||||
fn emit_string_enum(name: &str, variants: &[String]) -> String {
|
||||
let body = variants.iter()
|
||||
.filter_map(|v| v.as_str())
|
||||
.map(|v| {
|
||||
let ident = pascal_case(v);
|
||||
let rename = if ident == v {
|
||||
let rename = if ident == *v {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" #[serde(rename = {})]\n", json_str(v))
|
||||
@@ -365,40 +363,33 @@ fn emit_string_enum(name: &str, variants: &[serde_json::Value]) -> String {
|
||||
}
|
||||
|
||||
|
||||
fn emit_transparent_array(name: &str, schema: &JsonSchema, ctx: &mut EnumCtx) -> String {
|
||||
ctx.depth = 1;
|
||||
fn emit_transparent_array(name: &str, inner: &TypeShape, ctx: &mut EnumCtx) -> String {
|
||||
ctx.enum_name = None;
|
||||
let inner = rust_type_from_schema(schema.items.as_deref().unwrap_or(&JsonSchema::default()), ctx);
|
||||
let inner_ty = rust_type_from_shape(inner, ctx);
|
||||
format!(
|
||||
"#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(transparent)]\npub struct {name}(pub Vec<{inner}>);\n",
|
||||
"#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(transparent)]\npub struct {name}(pub Vec<{inner_ty}>);\n",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
fn emit_struct(
|
||||
fn emit_struct_decl(
|
||||
name: &str,
|
||||
schema: &JsonSchema,
|
||||
properties: &IndexMap<String, JsonSchema>,
|
||||
fields: &[IrStructField],
|
||||
ctx: &mut EnumCtx,
|
||||
) -> String {
|
||||
let required: std::collections::HashSet<&str> =
|
||||
schema.required.iter().map(String::as_str).collect();
|
||||
|
||||
let fields = properties.iter()
|
||||
.map(|(field_raw, field_schema)| {
|
||||
let field_name = rust_ident(field_raw);
|
||||
ctx.depth = 1;
|
||||
ctx.enum_name = Some(format!("{name}_{}", pascal_case(field_raw)));
|
||||
let mut ty = rust_type_from_schema(field_schema, ctx);
|
||||
let is_required = required.contains(field_raw.as_str())
|
||||
|| field_schema.default.is_some();
|
||||
let fields_body = fields.iter()
|
||||
.map(|f| {
|
||||
let field_name = rust_ident(&f.name);
|
||||
ctx.enum_name = Some(format!("{name}_{}", pascal_case(&f.name)));
|
||||
let mut ty = rust_type_from_shape(&f.shape, ctx);
|
||||
let is_required = f.required || f.default.is_some();
|
||||
if !is_required && !ty.starts_with("Option<") {
|
||||
ty = format!("Option<{ty}>");
|
||||
}
|
||||
let rename = if field_name == *field_raw {
|
||||
let rename = if field_name == f.name {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" #[serde(rename = \"{field_raw}\")]\n")
|
||||
format!(" #[serde(rename = \"{raw}\")]\n", raw = f.name)
|
||||
};
|
||||
format!("{rename} pub {field_name}: {ty},")
|
||||
})
|
||||
@@ -406,69 +397,48 @@ fn emit_struct(
|
||||
.join("\n");
|
||||
|
||||
format!(
|
||||
"#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct {name} {{\n{fields}\n}}\n",
|
||||
"#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct {name} {{\n{fields_body}\n}}\n",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
fn emit_type_alias(name: &str, schema: &JsonSchema, ctx: &mut EnumCtx) -> String {
|
||||
ctx.depth = 0;
|
||||
fn emit_type_alias(name: &str, inner: &TypeShape, ctx: &mut EnumCtx) -> String {
|
||||
ctx.enum_name = Some(name.to_string());
|
||||
let ty = rust_type_from_schema(schema, ctx);
|
||||
let ty = rust_type_from_shape(inner, ctx);
|
||||
format!("pub type {name} = {ty};\n")
|
||||
}
|
||||
|
||||
|
||||
fn rust_type_from_schema(schema: &JsonSchema, ctx: &mut EnumCtx) -> String {
|
||||
if let Some(r) = schema.ref_name() {
|
||||
return rust_type_ident(r);
|
||||
}
|
||||
|
||||
if let Some(any_of) = &schema.any_of {
|
||||
let has_null = any_of.iter().any(|s| s.ty.as_deref() == Some("null"));
|
||||
let non_null: Vec<&JsonSchema> = any_of
|
||||
.iter()
|
||||
.filter(|s| s.ty.as_deref() != Some("null"))
|
||||
.collect();
|
||||
if has_null && non_null.len() == 1 {
|
||||
fn rust_type_from_shape(shape: &TypeShape, ctx: &mut EnumCtx) -> String {
|
||||
match shape {
|
||||
TypeShape::Ref(name) => rust_type_ident(name),
|
||||
TypeShape::Primitive(Primitive::Integer) => "i64".to_string(),
|
||||
TypeShape::Primitive(Primitive::Number) => "f64".to_string(),
|
||||
TypeShape::Primitive(Primitive::Boolean) => "bool".to_string(),
|
||||
TypeShape::Primitive(Primitive::String) => "String".to_string(),
|
||||
TypeShape::List(inner) => {
|
||||
ctx.enum_name = None;
|
||||
return format!("Option<{}>", rust_type_from_schema(non_null[0], ctx));
|
||||
format!("Vec<{}>", rust_type_from_shape(inner, ctx))
|
||||
}
|
||||
}
|
||||
|
||||
let nullable = schema.nullable;
|
||||
let inner = inner_rust_type(schema, ctx);
|
||||
if nullable {
|
||||
format!("Option<{inner}>")
|
||||
} else {
|
||||
inner
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn inner_rust_type(schema: &JsonSchema, ctx: &mut EnumCtx) -> String {
|
||||
if let Some(values) = &schema.r#enum {
|
||||
if schema.ty.as_deref() == Some("string") {
|
||||
TypeShape::Optional(inner) => {
|
||||
ctx.enum_name = None;
|
||||
format!("Option<{}>", rust_type_from_shape(inner, ctx))
|
||||
}
|
||||
TypeShape::Enum(variants) => {
|
||||
// Inline enums hoist out into top-level Rust enum types so the
|
||||
// generated struct field can reference them by name.
|
||||
let enum_name = ctx
|
||||
.enum_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("Enum_{}", ctx.depth));
|
||||
ctx.hoisted.push((enum_name.clone(), values.clone()));
|
||||
return enum_name;
|
||||
.unwrap_or_else(|| "Enum_inline".to_string());
|
||||
ctx.hoisted.push((enum_name.clone(), variants.clone()));
|
||||
enum_name
|
||||
}
|
||||
}
|
||||
match schema.ty.as_deref() {
|
||||
Some("integer") => "i64".to_string(),
|
||||
Some("number") => "f64".to_string(),
|
||||
Some("boolean") => "bool".to_string(),
|
||||
Some("string") => "String".to_string(),
|
||||
Some("array") => {
|
||||
ctx.depth += 1;
|
||||
ctx.enum_name = None;
|
||||
let inner = rust_type_from_schema(schema.items.as_deref().unwrap_or(&JsonSchema::default()), ctx);
|
||||
format!("Vec<{inner}>")
|
||||
TypeShape::Union(_branches) => {
|
||||
// Rust serde doesn't have a clean way to deserialize an untagged
|
||||
// multi-arm union without losing type info; fall back to a JSON
|
||||
// Value so the consumer can match on the runtime variant.
|
||||
"serde_json::Value".to_string()
|
||||
}
|
||||
Some("object") => "serde_json::Value".to_string(),
|
||||
_ => "serde_json::Value".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user