//! `mizan-generate` — Rust codegen binary. //! //! Replaces the Node-based `protocol/mizan-generate/generator/cli.mjs`. //! Reads `mizan.toml`, spawns the configured backend to fetch the IR, and //! dispatches each `--target` to its `CodegenTarget` impl. Per-target file //! emission writes under the configured `output` directory. use std::fs; use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; use clap::Parser; use mizan_codegen::{config, emit, fetch}; #[derive(Parser, Debug)] #[command( name = "mizan-generate", about = "Mizan code generator — consumes Mizan IR; emits typed clients.", )] struct Cli { /// Path to the codegen config file. #[arg(short, long, default_value = "mizan.toml")] config: PathBuf, /// Output directory (overrides `output` in config). #[arg(short, long)] output: Option, /// Comma-separated list of targets (overrides `targets` in config). #[arg(short, long)] target: Option, /// Read the IR from a JSON file instead of spawning the backend's /// schema-export command. The fixture path used by integration tests. #[arg(long)] from_json: Option, } fn main() -> Result<()> { let cli = Cli::parse(); let config_exists = cli.config.exists(); let mut config: config::Config = if config_exists { let config_text = fs::read_to_string(&cli.config) .with_context(|| format!("reading config: {}", cli.config.display()))?; toml::from_str(&config_text) .with_context(|| format!("parsing TOML: {}", cli.config.display()))? } else if cli.from_json.is_some() { // --from-json bypasses the fetcher, so a missing config is fine — // CLI flags supply output + targets. config::Config { project_id: None, output: PathBuf::from("."), targets: vec![], source: Default::default(), rust_kernel: None, rust_crate_name: None, } } else { return Err(anyhow::anyhow!( "config not found: {} (pass --from-json to skip fetch)", cli.config.display(), )); }; if let Some(o) = cli.output { config.output = o; } if let Some(t) = cli.target { config.targets = t.split(',').map(|s| s.trim().to_string()).collect(); } let config_dir = if config_exists { resolve_config_dir(&cli.config)? } else { std::env::current_dir()? }; let ir = if let Some(json_path) = &cli.from_json { let abs = if json_path.is_absolute() { json_path.clone() } else { config_dir.join(json_path) }; eprintln!("[mizan] Reading IR from {}", abs.display()); let raw = fs::read_to_string(&abs) .with_context(|| format!("read {}", abs.display()))?; fetch::parse_ir_from_str(&raw)? } else { eprintln!("[mizan] Fetching schema..."); fetch::fetch_schema(&config, &config_dir)? }; eprintln!( "[mizan] Loaded {} function(s), {} context group(s), {} type(s)", ir.functions.len(), ir.contexts.len(), ir.types.len(), ); // Stage 1 is the framework-agnostic foundation that react/vue/svelte // import from. Auto-include it whenever any consumer of `./index` // (the Stage 1 re-export root) is in the target set. let needs_stage1 = config.targets.iter() .any(|t| matches!(t.as_str(), "react" | "vue" | "svelte")); if needs_stage1 && !config.targets.iter().any(|t| t == "stage1") { config.targets.insert(0, "stage1".to_string()); } // Channels schema piggybacks on the main schema (x-mizan-channels); // auto-include the channels emit when react is the target and the // schema actually carries channels. if config.targets.iter().any(|t| t == "react") && !ir.channels.is_empty() && !config.targets.iter().any(|t| t == "channels") { config.targets.push("channels".to_string()); } eprintln!("[mizan] Targets: {}", config.targets.join(", ")); let output_dir = if config.output.is_absolute() { config.output.clone() } else { config_dir.join(&config.output) }; for target_name in &config.targets { let Some(target) = emit::target_by_name(target_name) else { eprintln!("[mizan] WARN: target '{target_name}' has no emitter yet (Phase 2 scaffold)"); continue; }; let files = target.emit(&ir, &config); for file in files { let path = output_dir.join(&file.rel_path); write_output(&path, &file.content)?; eprintln!("[mizan] {} -> {}", target.name(), file.rel_path.display()); } } eprintln!("[mizan] Generation complete."); Ok(()) } fn resolve_config_dir(config_path: &Path) -> Result { let abs = fs::canonicalize(config_path) .with_context(|| format!("canonicalize {}", config_path.display()))?; Ok(abs .parent() .map(|p| p.to_path_buf()) .unwrap_or_else(|| PathBuf::from("."))) } fn write_output(path: &Path, content: &str) -> Result<()> { if let Some(parent) = path.parent() { fs::create_dir_all(parent) .with_context(|| format!("mkdir {}", parent.display()))?; } fs::write(path, content).with_context(|| format!("write {}", path.display())) }