Roll the working tree back to the last approved shape, before the post-LICENSE span that false-greened the AFI parity matrix with symbol-presence probes and smuggled an unauthorized SQLAlchemy dependency into FastAPI's Shapes binding.
Forward commit, not a history rewrite — the six commits since 4effcc7 stay in the log as the record of what happened.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
445 lines
14 KiB
Rust
445 lines
14 KiB
Rust
//! Rust target — emits a complete Cargo crate consuming the
|
|
//! `mizan-rust` kernel. Output shape lives at `templates/rust/*.j2`.
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use askama::Template;
|
|
use indexmap::IndexMap;
|
|
|
|
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, MizanContext, MizanFunction, MizanIR, NamedType, Primitive,
|
|
StructField as IrStructField, TypeShape,
|
|
};
|
|
|
|
|
|
pub struct RustCrate;
|
|
|
|
|
|
impl CodegenTarget for RustCrate {
|
|
fn name(&self) -> &'static str { "rust" }
|
|
|
|
fn emit(&self, ir: &MizanIR, config: &Config) -> Vec<EmittedFile> {
|
|
let crate_name = config
|
|
.rust_crate_name
|
|
.clone()
|
|
.unwrap_or_else(|| "mizan_client".to_string());
|
|
|
|
let kernel_dep = format_kernel_dep(config.rust_kernel.as_ref());
|
|
|
|
let mut out: Vec<EmittedFile> = Vec::new();
|
|
|
|
out.push(EmittedFile::new(
|
|
"Cargo.toml",
|
|
CargoTemplate { crate_name: &crate_name, kernel_dep: &kernel_dep }
|
|
.render().expect("Cargo.toml renders"),
|
|
));
|
|
|
|
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 {
|
|
let module_name = snake_case(ctx_name);
|
|
out.push(EmittedFile::new(
|
|
PathBuf::from("src/contexts").join(format!("{module_name}.rs")),
|
|
emit_context_file(ctx_name, ctx_meta, &ir.functions),
|
|
));
|
|
context_modules.push(module_name);
|
|
}
|
|
if !context_modules.is_empty() {
|
|
out.push(EmittedFile::new("src/contexts/mod.rs", emit_mod_file(&context_modules)));
|
|
}
|
|
|
|
let mut mutation_modules: Vec<String> = Vec::new();
|
|
let mut function_modules: Vec<String> = Vec::new();
|
|
for fn_meta in &ir.functions {
|
|
if !matches!(fn_meta.is_context, IsContext::No) || fn_meta.is_form { continue; }
|
|
let is_mutation = !fn_meta.affects.is_empty();
|
|
let kind = if is_mutation { "mutations" } else { "functions" };
|
|
let module_name = snake_case(&fn_meta.camel_name);
|
|
out.push(EmittedFile::new(
|
|
PathBuf::from(format!("src/{kind}")).join(format!("{module_name}.rs")),
|
|
emit_call_file(fn_meta),
|
|
));
|
|
if is_mutation {
|
|
mutation_modules.push(module_name);
|
|
} else {
|
|
function_modules.push(module_name);
|
|
}
|
|
}
|
|
if !mutation_modules.is_empty() {
|
|
out.push(EmittedFile::new("src/mutations/mod.rs", emit_mod_file(&mutation_modules)));
|
|
}
|
|
if !function_modules.is_empty() {
|
|
out.push(EmittedFile::new("src/functions/mod.rs", emit_mod_file(&function_modules)));
|
|
}
|
|
|
|
out.push(EmittedFile::new(
|
|
"src/lib.rs",
|
|
LibTemplate {
|
|
has_contexts: !context_modules.is_empty(),
|
|
has_mutations: !mutation_modules.is_empty(),
|
|
has_functions: !function_modules.is_empty(),
|
|
}.render().expect("lib.rs renders"),
|
|
));
|
|
|
|
out
|
|
}
|
|
}
|
|
|
|
|
|
#[derive(Template)]
|
|
#[template(path = "rust/Cargo.toml.j2", escape = "none")]
|
|
struct CargoTemplate<'a> {
|
|
crate_name: &'a str,
|
|
kernel_dep: &'a str,
|
|
}
|
|
|
|
|
|
#[derive(Template)]
|
|
#[template(path = "rust/lib.rs.j2", escape = "none")]
|
|
struct LibTemplate {
|
|
has_contexts: bool,
|
|
has_mutations: bool,
|
|
has_functions: bool,
|
|
}
|
|
|
|
|
|
#[derive(Template)]
|
|
#[template(path = "rust/mod.rs.j2", escape = "none")]
|
|
struct ModTemplate {
|
|
modules: Vec<String>,
|
|
}
|
|
|
|
|
|
#[derive(Template)]
|
|
#[template(path = "rust/context.rs.j2", escape = "none")]
|
|
struct ContextTemplate<'a> {
|
|
pascal: String,
|
|
snake: String,
|
|
ctx_name: &'a str,
|
|
type_imports: Vec<String>,
|
|
data_fields: Vec<RustField>,
|
|
params: Vec<RustField>,
|
|
}
|
|
|
|
|
|
#[derive(Template)]
|
|
#[template(path = "rust/call.rs.j2", escape = "none")]
|
|
struct CallTemplate<'a> {
|
|
snake: String,
|
|
name: &'a str,
|
|
return_type: String,
|
|
type_imports: Vec<String>,
|
|
input_param: String,
|
|
args_value: &'static str,
|
|
}
|
|
|
|
|
|
#[derive(Template)]
|
|
#[template(path = "rust/types.rs.j2", escape = "none")]
|
|
struct TypesTemplate {
|
|
schemas_block: String,
|
|
hoisted_enums_block: String,
|
|
}
|
|
|
|
|
|
/// 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,
|
|
has_rename: bool,
|
|
}
|
|
|
|
|
|
fn dedupe_preserving_order(items: impl IntoIterator<Item = String>) -> Vec<String> {
|
|
let mut seen = std::collections::HashSet::new();
|
|
items.into_iter().filter(|s| seen.insert(s.clone())).collect()
|
|
}
|
|
|
|
|
|
// ─── Cargo.toml ────────────────────────────────────────────────────────────
|
|
|
|
|
|
fn format_kernel_dep(spec: Option<&RustKernelSpec>) -> String {
|
|
match spec {
|
|
Some(RustKernelSpec::Path { path }) => format!("{{ path = {} }}", json_str(path)),
|
|
Some(RustKernelSpec::Git { git, tag, rev, branch }) => {
|
|
let mut parts = vec![format!("git = {}", json_str(git))];
|
|
if let Some(t) = tag { parts.push(format!("tag = {}", json_str(t))); }
|
|
if let Some(r) = rev { parts.push(format!("rev = {}", json_str(r))); }
|
|
if let Some(b) = branch { parts.push(format!("branch = {}", json_str(b))); }
|
|
format!("{{ {} }}", parts.join(", "))
|
|
}
|
|
Some(RustKernelSpec::Version { version }) => format!("{{ version = {} }}", json_str(version)),
|
|
None => "{ version = \"0.1\" }".to_string(),
|
|
}
|
|
}
|
|
|
|
|
|
fn json_str(s: &str) -> String {
|
|
serde_json::to_string(s).expect("string literal serializes")
|
|
}
|
|
|
|
|
|
// ─── mod.rs ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
fn emit_mod_file(module_names: &[String]) -> String {
|
|
let mut sorted = module_names.to_vec();
|
|
sorted.sort();
|
|
ModTemplate { modules: sorted }.render().expect("mod.rs renders")
|
|
}
|
|
|
|
|
|
// ─── Context file ──────────────────────────────────────────────────────────
|
|
|
|
|
|
fn emit_context_file(
|
|
ctx_name: &str,
|
|
ctx_meta: &MizanContext,
|
|
all_functions: &[MizanFunction],
|
|
) -> String {
|
|
let pascal = pascal_case(ctx_name);
|
|
let snake = snake_case(ctx_name);
|
|
|
|
let ctx_fns: Vec<&MizanFunction> = all_functions
|
|
.iter()
|
|
.filter(|f| f.is_context.as_str() == Some(ctx_name))
|
|
.collect();
|
|
|
|
let type_imports = dedupe_preserving_order(
|
|
ctx_fns.iter().map(|f| rust_type_ident(&f.output_type)),
|
|
);
|
|
|
|
let data_fields: Vec<RustField> = ctx_fns.iter()
|
|
.map(|f| {
|
|
let ident = rust_ident(&f.name);
|
|
RustField {
|
|
has_rename: ident != f.name,
|
|
raw_name: f.name.clone(),
|
|
ident,
|
|
ty: rust_type_ident(&f.output_type),
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
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 ty = if p_meta.required { base.to_string() } else { format!("Option<{base}>") };
|
|
RustField {
|
|
has_rename: ident != *p_name,
|
|
raw_name: p_name.clone(),
|
|
ident,
|
|
ty,
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
ContextTemplate {
|
|
pascal,
|
|
snake,
|
|
ctx_name,
|
|
type_imports,
|
|
data_fields,
|
|
params,
|
|
}.render().expect("context.rs renders")
|
|
}
|
|
|
|
|
|
fn param_rust_type(p: Primitive) -> &'static str {
|
|
match p {
|
|
Primitive::Integer => "i64",
|
|
Primitive::Number => "f64",
|
|
Primitive::Boolean => "bool",
|
|
Primitive::String => "String",
|
|
}
|
|
}
|
|
|
|
|
|
// ─── Call file ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
fn emit_call_file(fn_meta: &MizanFunction) -> String {
|
|
let output_type = rust_type_ident(&fn_meta.output_type);
|
|
let return_type = if fn_meta.output_nullable {
|
|
format!("Option<{output_type}>")
|
|
} else {
|
|
output_type.clone()
|
|
};
|
|
|
|
let input_type = fn_meta.input_type.as_deref().map(rust_type_ident);
|
|
|
|
let mut used_seed: Vec<String> = vec![output_type.clone()];
|
|
if let Some(t) = &input_type { used_seed.push(t.clone()); }
|
|
let type_imports = dedupe_preserving_order(used_seed);
|
|
|
|
let (input_param, args_value) = if fn_meta.has_input {
|
|
let it = input_type.as_deref().unwrap_or("");
|
|
(
|
|
format!(", args: &{it}"),
|
|
"serde_json::to_value(args).unwrap_or(Value::Object(Default::default()))",
|
|
)
|
|
} else {
|
|
(
|
|
String::new(),
|
|
"Value::Object(Default::default())",
|
|
)
|
|
};
|
|
|
|
CallTemplate {
|
|
snake: snake_case(&fn_meta.name),
|
|
name: &fn_meta.name,
|
|
return_type,
|
|
type_imports,
|
|
input_param,
|
|
args_value,
|
|
}.render().expect("call.rs renders")
|
|
}
|
|
|
|
|
|
// ─── 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<String>)>,
|
|
enum_name: Option<String>,
|
|
}
|
|
|
|
|
|
fn emit_types_rs(types: &IndexMap<String, NamedType>) -> String {
|
|
let mut ctx = EnumCtx { hoisted: Vec::new(), enum_name: None };
|
|
|
|
let schemas_block = types.iter()
|
|
.map(|(raw_name, ty)| {
|
|
let name = rust_type_ident(raw_name);
|
|
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),
|
|
}
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
|
|
let hoisted_enums_block = ctx.hoisted.iter()
|
|
.map(|(n, v)| emit_string_enum(n, v))
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
|
|
TypesTemplate { schemas_block, hoisted_enums_block }
|
|
.render().expect("types.rs renders")
|
|
}
|
|
|
|
|
|
fn emit_string_enum(name: &str, variants: &[String]) -> String {
|
|
let body = variants.iter()
|
|
.map(|v| {
|
|
let ident = pascal_case(v);
|
|
let rename = if ident == *v {
|
|
String::new()
|
|
} else {
|
|
format!(" #[serde(rename = {})]\n", json_str(v))
|
|
};
|
|
format!("{rename} {ident},")
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
format!(
|
|
"#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub enum {name} {{\n{body}\n}}\n",
|
|
name = rust_type_ident(name),
|
|
)
|
|
}
|
|
|
|
|
|
fn emit_transparent_array(name: &str, inner: &TypeShape, ctx: &mut EnumCtx) -> String {
|
|
ctx.enum_name = None;
|
|
let inner_ty = rust_type_from_shape(inner, ctx);
|
|
format!(
|
|
"#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(transparent)]\npub struct {name}(pub Vec<{inner_ty}>);\n",
|
|
)
|
|
}
|
|
|
|
|
|
fn emit_struct_decl(
|
|
name: &str,
|
|
fields: &[IrStructField],
|
|
ctx: &mut EnumCtx,
|
|
) -> String {
|
|
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 == f.name {
|
|
String::new()
|
|
} else {
|
|
format!(" #[serde(rename = \"{raw}\")]\n", raw = f.name)
|
|
};
|
|
format!("{rename} pub {field_name}: {ty},")
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
|
|
format!(
|
|
"#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct {name} {{\n{fields_body}\n}}\n",
|
|
)
|
|
}
|
|
|
|
|
|
fn emit_type_alias(name: &str, inner: &TypeShape, ctx: &mut EnumCtx) -> String {
|
|
ctx.enum_name = Some(name.to_string());
|
|
let ty = rust_type_from_shape(inner, ctx);
|
|
format!("pub type {name} = {ty};\n")
|
|
}
|
|
|
|
|
|
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;
|
|
format!("Vec<{}>", rust_type_from_shape(inner, ctx))
|
|
}
|
|
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(|| "Enum_inline".to_string());
|
|
ctx.hoisted.push((enum_name.clone(), variants.clone()));
|
|
enum_name
|
|
}
|
|
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()
|
|
}
|
|
}
|
|
}
|