Fixes/upgrades
This commit is contained in:
@@ -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,
|
||||
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user