diff --git a/Cargo.lock b/Cargo.lock index 73181aa..a4b1bed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -680,6 +680,9 @@ dependencies = [ "glob", "miette", "mlf-codegen", + "mlf-codegen-go", + "mlf-codegen-rust", + "mlf-codegen-typescript", "mlf-diagnostics", "mlf-lang", "mlf-validation", diff --git a/mlf-cli/Cargo.toml b/mlf-cli/Cargo.toml index 4623c2d..7266870 100644 --- a/mlf-cli/Cargo.toml +++ b/mlf-cli/Cargo.toml @@ -19,3 +19,14 @@ thiserror = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" glob = "0.3" + +# Optional code generator plugins +mlf-codegen-typescript = { path = "../codegen-plugins/mlf-codegen-typescript", optional = true } +mlf-codegen-go = { path = "../codegen-plugins/mlf-codegen-go", optional = true } +mlf-codegen-rust = { path = "../codegen-plugins/mlf-codegen-rust", optional = true } + +[features] +default = ["typescript", "go", "rust"] +typescript = ["dep:mlf-codegen-typescript"] +go = ["dep:mlf-codegen-go"] +rust = ["dep:mlf-codegen-rust"] diff --git a/mlf-cli/src/generate/code.rs b/mlf-cli/src/generate/code.rs new file mode 100644 index 0000000..f5f3be6 --- /dev/null +++ b/mlf-cli/src/generate/code.rs @@ -0,0 +1,225 @@ +use miette::Diagnostic; +use mlf_codegen::plugin::{CodeGenerator, GeneratorContext}; +use std::path::{Path, PathBuf}; +use thiserror::Error; + +#[derive(Error, Debug, Diagnostic)] +pub enum GenerateError { + #[error("Failed to parse lexicon: {path}")] + #[diagnostic(code(mlf::generate::parse_lexicon))] + ParseLexicon { + path: String, + #[help] + help: Option, + }, + + #[error("Failed to write output: {path}")] + #[diagnostic(code(mlf::generate::write_output))] + WriteOutput { + path: String, + #[source] + source: std::io::Error, + }, + + #[error("Failed to expand glob pattern")] + #[diagnostic(code(mlf::generate::glob_error))] + GlobError { + #[source] + source: glob::GlobError, + }, + + #[error("Invalid glob pattern: {pattern}")] + #[diagnostic(code(mlf::generate::invalid_glob))] + InvalidGlob { + pattern: String, + #[source] + source: glob::PatternError, + }, + + #[error("Generator '{name}' not found")] + #[diagnostic(code(mlf::generate::generator_not_found))] + #[help("Available generators: {}", available.join(", "))] + GeneratorNotFound { + name: String, + available: Vec, + }, + + #[error("Code generation failed: {0}")] + #[diagnostic(code(mlf::generate::generation_failed))] + GenerationFailed(String), +} + +pub fn run( + generator_name: String, + input_patterns: Vec, + output_dir: PathBuf, + flat: bool, +) -> Result<(), GenerateError> { + // Find the generator + let generators = mlf_codegen::plugin::generators(); + let generator = generators + .iter() + .find(|g| g.name() == generator_name) + .ok_or_else(|| { + let available: Vec = generators.iter().map(|g| g.name().to_string()).collect(); + GenerateError::GeneratorNotFound { + name: generator_name.clone(), + available, + } + })?; + + println!("Using generator: {} ({})", generator.name(), generator.description()); + println!("Output extension: {}\n", generator.file_extension()); + + // Collect input files + let mut file_paths = Vec::new(); + for pattern in input_patterns { + if pattern.contains('*') || pattern.contains('?') { + for entry in glob::glob(&pattern).map_err(|source| GenerateError::InvalidGlob { + pattern: pattern.clone(), + source, + })? { + let path = entry.map_err(|source| GenerateError::GlobError { source })?; + file_paths.push(path); + } + } else { + file_paths.push(PathBuf::from(pattern)); + } + } + + std::fs::create_dir_all(&output_dir).map_err(|source| GenerateError::WriteOutput { + path: output_dir.display().to_string(), + source, + })?; + + let mut errors = Vec::new(); + let mut success_count = 0; + + for file_path in file_paths { + let source = match std::fs::read_to_string(&file_path) { + Ok(s) => s, + Err(e) => { + errors.push(( + file_path.display().to_string(), + format!("Failed to read file: {}", e), + )); + continue; + } + }; + + let lexicon = match mlf_lang::parse_lexicon(&source) { + Ok(lex) => lex, + Err(e) => { + errors.push((file_path.display().to_string(), format!("{:?}", e))); + continue; + } + }; + + let namespace = extract_namespace(&file_path); + + // Create workspace with standard library + let mut workspace = match mlf_lang::Workspace::with_std() { + Ok(ws) => ws, + Err(e) => { + errors.push(( + file_path.display().to_string(), + format!("Failed to load standard library: {:?}", e), + )); + continue; + } + }; + + // Add the module to the workspace + if let Err(e) = workspace.add_module(namespace.clone(), lexicon.clone()) { + errors.push(( + file_path.display().to_string(), + format!("Failed to add module: {:?}", e), + )); + continue; + } + + // Resolve types + if let Err(e) = workspace.resolve() { + errors.push(( + file_path.display().to_string(), + format!("Type resolution error: {:?}", e), + )); + continue; + } + + // Generate code using the selected generator + let ctx = GeneratorContext { + namespace: &namespace, + lexicon: &lexicon, + workspace: &workspace, + }; + + let generated_code = match generator.generate(&ctx) { + Ok(code) => code, + Err(e) => { + errors.push((file_path.display().to_string(), format!("Generation error: {}", e))); + continue; + } + }; + + // Determine output path + let output_path = if flat { + let filename = format!("{}{}", namespace, generator.file_extension()); + output_dir.join(filename) + } else { + let mut path = output_dir.clone(); + for segment in namespace.split('.') { + path.push(segment); + } + if let Err(e) = std::fs::create_dir_all(&path.parent().unwrap()) { + errors.push(( + file_path.display().to_string(), + format!("Failed to create directory: {}", e), + )); + continue; + } + path.set_extension(generator.file_extension().trim_start_matches('.')); + path + }; + + // Write generated code + if let Err(source) = std::fs::write(&output_path, generated_code) { + errors.push(( + output_path.display().to_string(), + format!("Failed to write file: {}", source), + )); + continue; + } + + println!("Generated: {}", output_path.display()); + success_count += 1; + } + + if !errors.is_empty() { + eprintln!( + "\n{} file(s) generated successfully, {} error(s) encountered:\n", + success_count, + errors.len() + ); + for (path, error) in &errors { + eprintln!(" {} - {}", path, error); + } + eprintln!(); + return Err(GenerateError::ParseLexicon { + path: "multiple files".to_string(), + help: Some(format!("{} errors total", errors.len())), + }); + } + + println!("\nSuccessfully generated {} file(s)", success_count); + Ok(()) +} + +fn extract_namespace(file_path: &Path) -> String { + // Namespace is derived solely from the filename + file_path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("unknown") + .to_string() +} diff --git a/mlf-cli/src/generate.rs b/mlf-cli/src/generate/mod.rs similarity index 54% rename from mlf-cli/src/generate.rs rename to mlf-cli/src/generate/mod.rs index d774353..1873bc9 100644 --- a/mlf-cli/src/generate.rs +++ b/mlf-cli/src/generate/mod.rs @@ -1 +1,2 @@ +pub mod code; pub mod lexicon; diff --git a/mlf-cli/src/main.rs b/mlf-cli/src/main.rs index 9a545e7..bfe4c25 100644 --- a/mlf-cli/src/main.rs +++ b/mlf-cli/src/main.rs @@ -6,6 +6,17 @@ use std::process; mod check; mod generate; +// Import optional code generator plugins +// These are automatically registered via inventory when the feature is enabled +#[cfg(feature = "typescript")] +use mlf_codegen_typescript as _; + +#[cfg(feature = "go")] +use mlf_codegen_go as _; + +#[cfg(feature = "rust")] +use mlf_codegen_rust as _; + #[derive(Parser)] #[command(name = "mlf")] #[command(about = "MLF (Matt's Lexicon Format) CLI tool", long_about = None)] @@ -47,6 +58,19 @@ enum GenerateCommands { #[arg(long, help = "Use flat file structure (e.g., app.bsky.post.json)")] flat: bool, }, + Code { + #[arg(short, long, help = "Generator to use (json, typescript, go, rust)")] + generator: String, + + #[arg(short, long, help = "Input MLF files (glob patterns supported)")] + input: Vec, + + #[arg(short, long, help = "Output directory")] + output: PathBuf, + + #[arg(long, help = "Use flat file structure (e.g., app.bsky.post.ts)")] + flat: bool, + }, } fn main() { @@ -63,6 +87,9 @@ fn main() { GenerateCommands::Lexicon { input, output, flat } => { generate::lexicon::run(input, output, flat).into_diagnostic() } + GenerateCommands::Code { generator, input, output, flat } => { + generate::code::run(generator, input, output, flat).into_diagnostic() + } }, }; diff --git a/tree-sitter-mlf/test.mlf b/tree-sitter-mlf/test.mlf index 9d27f56..5e5c566 100644 --- a/tree-sitter-mlf/test.mlf +++ b/tree-sitter-mlf/test.mlf @@ -7,4 +7,4 @@ record post { }, /// Creation timestamp createdAt: Datetime, -}; +}