//! Codegen configuration — deserialized from `mizan.toml` at the consumer //! project root. //! //! Example: //! //! ```toml //! project_id = "blazr-studio" //! output = "src/api" //! targets = ["react"] //! //! [source.fastapi] //! module = "blazr_session.handlers" //! cwd = "../.." //! command = ["uv", "run", "python"] //! //! [rust_kernel] //! path = "../../mizan/frontends/mizan-rust" //! ``` use std::collections::BTreeMap; use std::path::PathBuf; use serde::{Deserialize, Deserializer}; /// Every field carries a value once deserialization returns — a key absent /// from the TOML takes the corresponding field of `Config::default()`. #[derive(Debug, Deserialize)] #[serde(default)] pub struct Config { pub project_id: Option, pub output: PathBuf, pub targets: Vec, pub source: SourceConfig, pub rust_kernel: Option, pub rust_crate_name: String, } impl Default for Config { fn default() -> Self { Self { project_id: None, output: PathBuf::from("src/api"), targets: vec!["react".to_string()], source: SourceConfig::default(), rust_kernel: None, rust_crate_name: "mizan_client".to_string(), } } } fn default_python() -> String { "python".to_string() } /// A subprocess invocation split into the program and its argv. The /// deserializer rejects an empty array, so every `CommandLine` in hand names /// a program. #[derive(Debug, Clone)] pub struct CommandLine { program: String, args: Vec, } impl CommandLine { pub fn program_only(program: &str) -> Self { Self { program: program.to_string(), args: Vec::new() } } pub fn program(&self) -> &str { &self.program } pub fn args(&self) -> &[String] { &self.args } } impl<'de> Deserialize<'de> for CommandLine { fn deserialize>(deserializer: D) -> Result { let mut parts = Vec::::deserialize(deserializer)?.into_iter(); match parts.next() { Some(program) => Ok(CommandLine { program, args: parts.collect() }), None => Err(serde::de::Error::custom( "command must be a non-empty array naming the program first", )), } } } #[derive(Debug, Deserialize, Default)] pub struct SourceConfig { #[serde(default)] pub fastapi: Option, #[serde(default)] pub django: Option, #[serde(default)] pub rust: Option, /// `[source.script]` — spawn an arbitrary command and read its stdout /// as KDL IR. #[serde(default)] pub script: Option, } #[derive(Debug, Deserialize)] pub struct FastapiSource { pub module: String, #[serde(default)] pub cwd: Option, #[serde(default = "default_python")] pub python: String, #[serde(default)] pub command: Option, #[serde(default)] pub env: BTreeMap, } #[derive(Debug, Deserialize)] pub struct DjangoSource { pub manage_path: PathBuf, #[serde(default = "default_python")] pub python: String, #[serde(default)] pub command: Option, #[serde(default)] pub env: BTreeMap, } /// `[source.rust]` — spawn a Cargo binary that emits the Mizan IR (KDL) /// to stdout. The binary uses `mizan_core::build_ir()` after force-linking /// the consumer crate's `#[derive(Mizan)]` types and `#[mizan::client]` /// functions. #[derive(Debug, Deserialize)] pub struct RustSource { /// Path to the consumer's Cargo.toml, relative to the codegen config /// directory. #[serde(default = "default_manifest_path")] pub manifest_path: PathBuf, /// Name of the binary under `[[bin]]` that exports the IR. #[serde(default = "default_rust_bin")] pub bin: String, /// Cargo features to enable when building the bin. #[serde(default)] pub features: Vec, /// Build in release mode. #[serde(default)] pub release: bool, /// Environment overrides for the cargo subprocess. #[serde(default)] pub env: BTreeMap, /// Pre-step run before the Cargo bin: decoru writes Rust types from a /// Pydantic source module to `pydantic.output`. #[serde(default)] pub pydantic: Option, } fn default_manifest_path() -> PathBuf { PathBuf::from("Cargo.toml") } fn default_rust_bin() -> String { "emit-mizan-ir".to_string() } /// Pydantic → Rust pre-step. Runs an embedded Python helper that reports /// the module's `BaseModel` and `Enum` declarations, then writes the Rust /// file those shapes render to. #[derive(Debug, Deserialize)] pub struct PydanticPreStep { /// Python module name to import (e.g. `claude_manage.schema`). pub module: String, /// Path to write the generated Rust file, relative to the codegen /// config directory. pub output: PathBuf, /// Working directory for the python subprocess, relative to the /// codegen config directory. Defaults to the config directory itself. /// The script prepends this to `sys.path` so the module imports. #[serde(default)] pub cwd: Option, /// Python executable. #[serde(default = "default_python")] pub python: String, /// Full command override (e.g. `["uv", "run", "python"]`). Wins over /// `python` when present. #[serde(default)] pub command: Option, /// Derive macros applied to every generated struct. #[serde(default = "default_pydantic_derives")] pub derives: Vec, /// Prelude inserted at the top of the generated file — the leading /// comment plus `use` statements for referenced types decoru does not /// itself produce. #[serde(default)] pub header: String, /// Environment overrides for the python subprocess. #[serde(default)] pub env: BTreeMap, } fn default_pydantic_derives() -> Vec { vec![ "Debug".to_string(), "Clone".to_string(), "::serde::Serialize".to_string(), "::serde::Deserialize".to_string(), "::mizan_core::Mizan".to_string(), ] } /// `[source.script]` — spawns `command`, reads its stdout, and parses it as /// KDL Mizan IR. /// /// Example: /// /// ```toml /// [source.script] /// command = ["uv", "run", "python", "-m", "holomorphic.emit_ir"] /// ``` #[derive(Debug, Deserialize)] pub struct ScriptSource { /// Program plus argv. pub command: CommandLine, /// Working directory for the subprocess, relative to the codegen /// config directory. Defaults to the config directory itself. #[serde(default)] pub cwd: Option, /// Environment overrides. #[serde(default)] pub env: BTreeMap, } #[derive(Debug, Deserialize, Clone)] #[serde(untagged)] pub enum RustKernelSpec { Path { path: String, }, Git { git: String, #[serde(default)] tag: Option, #[serde(default)] rev: Option, #[serde(default)] branch: Option, }, Version { version: String, }, }