Fixes/upgrades

This commit is contained in:
2026-07-30 21:12:29 -04:00
parent e00b3a177e
commit 9494549861
13 changed files with 823 additions and 60 deletions

View File

@@ -177,8 +177,8 @@ pub struct RustSource {
#[serde(default)]
pub env: BTreeMap<String, String>,
/// Pre-step run before the Cargo bin: decoru writes Rust types from a
/// Pydantic source module to `pydantic.output`.
/// Pre-step run before the Cargo bin: writes Rust types from a Pydantic
/// source module to `pydantic.output`.
#[serde(default)]
pub pydantic: Option<PydanticPreStep>,
}
@@ -226,8 +226,8 @@ pub struct PydanticPreStep {
pub derives: Vec<String>,
/// Prelude inserted at the top of the generated file — the leading
/// comment plus `use` statements for referenced types decoru does not
/// itself produce.
/// comment plus `use` statements for referenced types the converter does
/// not itself produce.
#[serde(default)]
pub header: String,

View File

@@ -5,14 +5,17 @@
//!
//! - FastAPI: `python -m mizan_fastapi.ir <module>`
//! - Django: `python manage.py export_mizan_ir`
//!
//! - Rust: `cargo run --bin <bin>`, a consumer-side binary that
//! force-links its `#[derive(Mizan)]` types and `#[mizan::client]`
//! functions, then calls `mizan_core::build_ir()`.
//!
//! The Rust source supports an optional `[source.rust.pydantic]`
//! pre-step: a Python helper reports the module's Pydantic and Enum
//! declarations, and this module renders them into the Rust file the
//! `[source.rust.pydantic]` adds a pre-step: a helper reports the module's
//! Pydantic and Enum declarations, which render into the Rust file the
//! cargo bin then compiles against.
//!
//! That converter lives under `python/`, so the pre-step needs only an
//! interpreter carrying pydantic and jinja2.
use std::collections::BTreeMap;
use std::fs;
@@ -32,7 +35,46 @@ use crate::ir::{parse_ir, MizanIR};
/// Bridge script handed to the configured Python interpreter when
/// `[source.rust.pydantic]` is set.
const DECORU_BRIDGE_SCRIPT: &str = include_str!("../scripts/run_decoru.py");
const BRIDGE_SCRIPT: &str = include_str!("../scripts/run_pydantic_to_rust.py");
/// The `pydantic_to_rust` package, embedded so the pre-step needs only an
/// interpreter carrying pydantic and jinja2. Keys are paths relative to the
/// directory the bridge is materialized into.
const CONVERTER_SOURCES: &[(&str, &str)] = &[
(
"pydantic_to_rust/__init__.py",
include_str!("../python/pydantic_to_rust/__init__.py"),
),
(
"pydantic_to_rust/ir.py",
include_str!("../python/pydantic_to_rust/ir.py"),
),
(
"pydantic_to_rust/idents.py",
include_str!("../python/pydantic_to_rust/idents.py"),
),
(
"pydantic_to_rust/walker.py",
include_str!("../python/pydantic_to_rust/walker.py"),
),
(
"pydantic_to_rust/emit.py",
include_str!("../python/pydantic_to_rust/emit.py"),
),
(
"pydantic_to_rust/templates/struct.rs.j2",
include_str!("../python/pydantic_to_rust/templates/struct.rs.j2"),
),
(
"pydantic_to_rust/templates/type.rs.j2",
include_str!("../python/pydantic_to_rust/templates/type.rs.j2"),
),
(
"pydantic_to_rust/templates/default.rs.j2",
include_str!("../python/pydantic_to_rust/templates/default.rs.j2"),
),
];
pub fn fetch_schema(config: &Config, config_dir: &Path) -> Result<MizanIR> {
@@ -182,69 +224,82 @@ fn run_rust(src: &RustSource, config_dir: &Path) -> Result<String> {
#[derive(Deserialize)]
struct DecoruDiscovery {
enums: Vec<DecoruEnumSpec>,
struct Discovery {
enums: Vec<EnumSpec>,
structs: Vec<String>,
}
#[derive(Deserialize)]
struct DecoruEnumSpec {
struct EnumSpec {
name: String,
variants: Vec<String>,
}
#[derive(Template)]
#[template(path = "decoru/enum.rs.j2", escape = "none")]
struct DecoruEnumTemplate<'a> {
#[template(path = "pydantic_to_rust/enum.rs.j2", escape = "none")]
struct EnumTemplate<'a> {
name: &'a str,
derives: Vec<String>,
variants: Vec<DecoruVariant<'a>>,
variants: Vec<EnumVariant<'a>>,
}
struct DecoruVariant<'a> {
struct EnumVariant<'a> {
ident: &'a str,
is_default: bool,
}
#[derive(Template)]
#[template(path = "decoru/schema.rs.j2", escape = "none")]
struct DecoruSchemaTemplate<'a> {
#[template(path = "pydantic_to_rust/schema.rs.j2", escape = "none")]
struct SchemaTemplate<'a> {
header: &'a str,
blocks: Vec<String>,
}
fn render_decoru_enum(spec: &DecoruEnumSpec, derives: &[String]) -> String {
// decoru puts `impl Default` on every struct it emits, so an enum-typed
// field with no Pydantic default still has to satisfy `T::default()`.
// The last member carries `#[default]` to keep the file compiling.
fn render_enum(spec: &EnumSpec, derives: &[String]) -> String {
// Every emitted struct carries `impl Default`, so an enum-typed field
// with no Pydantic default still has to satisfy `T::default()`. The last
// member takes `#[default]` to keep the file compiling.
let last = spec.variants.len().saturating_sub(1);
let variants = spec.variants.iter()
.enumerate()
.map(|(i, ident)| DecoruVariant { ident, is_default: i == last })
.map(|(i, ident)| EnumVariant { ident, is_default: i == last })
.collect();
let mut derives = derives.to_vec();
derives.push("Default".to_string());
DecoruEnumTemplate { name: &spec.name, derives, variants }
EnumTemplate { name: &spec.name, derives, variants }
.render()
.unwrap_or_else(|e| panic!("template decoru/enum.rs.j2: {e}"))
.unwrap_or_else(|e| panic!("template pydantic_to_rust/enum.rs.j2: {e}"))
}
/// Materialize the bridge script so the interpreter can be handed a path
/// instead of a stdin pipe.
fn write_bridge_script() -> Result<PathBuf> {
let path = std::env::temp_dir()
.join(format!("mizan-decoru-bridge-{}.py", std::process::id()));
fs::write(&path, DECORU_BRIDGE_SCRIPT)
.with_context(|| format!("writing decoru bridge to {}", path.display()))?;
Ok(path)
/// Write the bridge and the converter beside each other, returning the script
/// path. Python puts a script's own directory on `sys.path`, which is what
/// `from pydantic_to_rust import ...` resolves against.
fn materialize_bridge() -> Result<PathBuf> {
let root = std::env::temp_dir()
.join(format!("mizan-pydantic-to-rust-{}", std::process::id()));
for (relative, source) in CONVERTER_SOURCES {
let path = root.join(relative);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("mkdir {}", parent.display()))?;
}
fs::write(&path, source)
.with_context(|| format!("writing converter to {}", path.display()))?;
}
let script = root.join("run_pydantic_to_rust.py");
fs::write(&script, BRIDGE_SCRIPT)
.with_context(|| format!("writing bridge to {}", script.display()))?;
Ok(script)
}
@@ -263,27 +318,33 @@ fn run_pydantic_prestep(src: &PydanticPreStep, config_dir: &Path) -> Result<()>
})
.to_string();
let script_path = write_bridge_script()?;
let script_path = materialize_bridge()?;
let command = interpreter(&src.command, &src.python);
let mut args = command.args().to_vec();
args.push(script_path.to_string_lossy().into_owned());
args.push(payload);
let stdout = run_subprocess(command.program(), &args, &cwd, &src.env, "decoru bridge")?;
let stdout = run_subprocess(
command.program(),
&args,
&cwd,
&src.env,
"pydantic-to-rust bridge",
)?;
let discovery: DecoruDiscovery = serde_json::from_str(&stdout)
.context("decoding the decoru bridge's JSON report")?;
let discovery: Discovery = serde_json::from_str(&stdout)
.context("decoding the bridge's JSON report")?;
let mut blocks: Vec<String> = discovery.enums.iter()
.map(|spec| render_decoru_enum(spec, &src.derives))
.map(|spec| render_enum(spec, &src.derives))
.collect();
let enum_count = blocks.len();
let struct_count = discovery.structs.len();
blocks.extend(discovery.structs);
let rendered = DecoruSchemaTemplate { header: &src.header, blocks }
let rendered = SchemaTemplate { header: &src.header, blocks }
.render()
.unwrap_or_else(|e| panic!("template decoru/schema.rs.j2: {e}"));
.unwrap_or_else(|e| panic!("template pydantic_to_rust/schema.rs.j2: {e}"));
if let Some(parent) = output_abs.parent() {
fs::create_dir_all(parent)
@@ -293,7 +354,7 @@ fn run_pydantic_prestep(src: &PydanticPreStep, config_dir: &Path) -> Result<()>
.with_context(|| format!("write {}", output_abs.display()))?;
eprintln!(
"[mizan] decoru: {enum_count} enum(s) + {struct_count} struct(s) -> {}",
"[mizan] pydantic: {enum_count} enum(s) + {struct_count} struct(s) -> {}",
output_abs.display(),
);