A human-friendly DSL for ATProto Lexicons
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Code generators in CLI

+269 -2
+3
Cargo.lock
··· 680 680 "glob", 681 681 "miette", 682 682 "mlf-codegen", 683 + "mlf-codegen-go", 684 + "mlf-codegen-rust", 685 + "mlf-codegen-typescript", 683 686 "mlf-diagnostics", 684 687 "mlf-lang", 685 688 "mlf-validation",
+11
mlf-cli/Cargo.toml
··· 19 19 serde = { version = "1", features = ["derive"] } 20 20 serde_json = "1" 21 21 glob = "0.3" 22 + 23 + # Optional code generator plugins 24 + mlf-codegen-typescript = { path = "../codegen-plugins/mlf-codegen-typescript", optional = true } 25 + mlf-codegen-go = { path = "../codegen-plugins/mlf-codegen-go", optional = true } 26 + mlf-codegen-rust = { path = "../codegen-plugins/mlf-codegen-rust", optional = true } 27 + 28 + [features] 29 + default = ["typescript", "go", "rust"] 30 + typescript = ["dep:mlf-codegen-typescript"] 31 + go = ["dep:mlf-codegen-go"] 32 + rust = ["dep:mlf-codegen-rust"]
-1
mlf-cli/src/generate.rs
··· 1 - pub mod lexicon;
+225
mlf-cli/src/generate/code.rs
··· 1 + use miette::Diagnostic; 2 + use mlf_codegen::plugin::{CodeGenerator, GeneratorContext}; 3 + use std::path::{Path, PathBuf}; 4 + use thiserror::Error; 5 + 6 + #[derive(Error, Debug, Diagnostic)] 7 + pub enum GenerateError { 8 + #[error("Failed to parse lexicon: {path}")] 9 + #[diagnostic(code(mlf::generate::parse_lexicon))] 10 + ParseLexicon { 11 + path: String, 12 + #[help] 13 + help: Option<String>, 14 + }, 15 + 16 + #[error("Failed to write output: {path}")] 17 + #[diagnostic(code(mlf::generate::write_output))] 18 + WriteOutput { 19 + path: String, 20 + #[source] 21 + source: std::io::Error, 22 + }, 23 + 24 + #[error("Failed to expand glob pattern")] 25 + #[diagnostic(code(mlf::generate::glob_error))] 26 + GlobError { 27 + #[source] 28 + source: glob::GlobError, 29 + }, 30 + 31 + #[error("Invalid glob pattern: {pattern}")] 32 + #[diagnostic(code(mlf::generate::invalid_glob))] 33 + InvalidGlob { 34 + pattern: String, 35 + #[source] 36 + source: glob::PatternError, 37 + }, 38 + 39 + #[error("Generator '{name}' not found")] 40 + #[diagnostic(code(mlf::generate::generator_not_found))] 41 + #[help("Available generators: {}", available.join(", "))] 42 + GeneratorNotFound { 43 + name: String, 44 + available: Vec<String>, 45 + }, 46 + 47 + #[error("Code generation failed: {0}")] 48 + #[diagnostic(code(mlf::generate::generation_failed))] 49 + GenerationFailed(String), 50 + } 51 + 52 + pub fn run( 53 + generator_name: String, 54 + input_patterns: Vec<String>, 55 + output_dir: PathBuf, 56 + flat: bool, 57 + ) -> Result<(), GenerateError> { 58 + // Find the generator 59 + let generators = mlf_codegen::plugin::generators(); 60 + let generator = generators 61 + .iter() 62 + .find(|g| g.name() == generator_name) 63 + .ok_or_else(|| { 64 + let available: Vec<String> = generators.iter().map(|g| g.name().to_string()).collect(); 65 + GenerateError::GeneratorNotFound { 66 + name: generator_name.clone(), 67 + available, 68 + } 69 + })?; 70 + 71 + println!("Using generator: {} ({})", generator.name(), generator.description()); 72 + println!("Output extension: {}\n", generator.file_extension()); 73 + 74 + // Collect input files 75 + let mut file_paths = Vec::new(); 76 + for pattern in input_patterns { 77 + if pattern.contains('*') || pattern.contains('?') { 78 + for entry in glob::glob(&pattern).map_err(|source| GenerateError::InvalidGlob { 79 + pattern: pattern.clone(), 80 + source, 81 + })? { 82 + let path = entry.map_err(|source| GenerateError::GlobError { source })?; 83 + file_paths.push(path); 84 + } 85 + } else { 86 + file_paths.push(PathBuf::from(pattern)); 87 + } 88 + } 89 + 90 + std::fs::create_dir_all(&output_dir).map_err(|source| GenerateError::WriteOutput { 91 + path: output_dir.display().to_string(), 92 + source, 93 + })?; 94 + 95 + let mut errors = Vec::new(); 96 + let mut success_count = 0; 97 + 98 + for file_path in file_paths { 99 + let source = match std::fs::read_to_string(&file_path) { 100 + Ok(s) => s, 101 + Err(e) => { 102 + errors.push(( 103 + file_path.display().to_string(), 104 + format!("Failed to read file: {}", e), 105 + )); 106 + continue; 107 + } 108 + }; 109 + 110 + let lexicon = match mlf_lang::parse_lexicon(&source) { 111 + Ok(lex) => lex, 112 + Err(e) => { 113 + errors.push((file_path.display().to_string(), format!("{:?}", e))); 114 + continue; 115 + } 116 + }; 117 + 118 + let namespace = extract_namespace(&file_path); 119 + 120 + // Create workspace with standard library 121 + let mut workspace = match mlf_lang::Workspace::with_std() { 122 + Ok(ws) => ws, 123 + Err(e) => { 124 + errors.push(( 125 + file_path.display().to_string(), 126 + format!("Failed to load standard library: {:?}", e), 127 + )); 128 + continue; 129 + } 130 + }; 131 + 132 + // Add the module to the workspace 133 + if let Err(e) = workspace.add_module(namespace.clone(), lexicon.clone()) { 134 + errors.push(( 135 + file_path.display().to_string(), 136 + format!("Failed to add module: {:?}", e), 137 + )); 138 + continue; 139 + } 140 + 141 + // Resolve types 142 + if let Err(e) = workspace.resolve() { 143 + errors.push(( 144 + file_path.display().to_string(), 145 + format!("Type resolution error: {:?}", e), 146 + )); 147 + continue; 148 + } 149 + 150 + // Generate code using the selected generator 151 + let ctx = GeneratorContext { 152 + namespace: &namespace, 153 + lexicon: &lexicon, 154 + workspace: &workspace, 155 + }; 156 + 157 + let generated_code = match generator.generate(&ctx) { 158 + Ok(code) => code, 159 + Err(e) => { 160 + errors.push((file_path.display().to_string(), format!("Generation error: {}", e))); 161 + continue; 162 + } 163 + }; 164 + 165 + // Determine output path 166 + let output_path = if flat { 167 + let filename = format!("{}{}", namespace, generator.file_extension()); 168 + output_dir.join(filename) 169 + } else { 170 + let mut path = output_dir.clone(); 171 + for segment in namespace.split('.') { 172 + path.push(segment); 173 + } 174 + if let Err(e) = std::fs::create_dir_all(&path.parent().unwrap()) { 175 + errors.push(( 176 + file_path.display().to_string(), 177 + format!("Failed to create directory: {}", e), 178 + )); 179 + continue; 180 + } 181 + path.set_extension(generator.file_extension().trim_start_matches('.')); 182 + path 183 + }; 184 + 185 + // Write generated code 186 + if let Err(source) = std::fs::write(&output_path, generated_code) { 187 + errors.push(( 188 + output_path.display().to_string(), 189 + format!("Failed to write file: {}", source), 190 + )); 191 + continue; 192 + } 193 + 194 + println!("Generated: {}", output_path.display()); 195 + success_count += 1; 196 + } 197 + 198 + if !errors.is_empty() { 199 + eprintln!( 200 + "\n{} file(s) generated successfully, {} error(s) encountered:\n", 201 + success_count, 202 + errors.len() 203 + ); 204 + for (path, error) in &errors { 205 + eprintln!(" {} - {}", path, error); 206 + } 207 + eprintln!(); 208 + return Err(GenerateError::ParseLexicon { 209 + path: "multiple files".to_string(), 210 + help: Some(format!("{} errors total", errors.len())), 211 + }); 212 + } 213 + 214 + println!("\nSuccessfully generated {} file(s)", success_count); 215 + Ok(()) 216 + } 217 + 218 + fn extract_namespace(file_path: &Path) -> String { 219 + // Namespace is derived solely from the filename 220 + file_path 221 + .file_stem() 222 + .and_then(|s| s.to_str()) 223 + .unwrap_or("unknown") 224 + .to_string() 225 + }
+2
mlf-cli/src/generate/mod.rs
··· 1 + pub mod code; 2 + pub mod lexicon;
+27
mlf-cli/src/main.rs
··· 6 6 mod check; 7 7 mod generate; 8 8 9 + // Import optional code generator plugins 10 + // These are automatically registered via inventory when the feature is enabled 11 + #[cfg(feature = "typescript")] 12 + use mlf_codegen_typescript as _; 13 + 14 + #[cfg(feature = "go")] 15 + use mlf_codegen_go as _; 16 + 17 + #[cfg(feature = "rust")] 18 + use mlf_codegen_rust as _; 19 + 9 20 #[derive(Parser)] 10 21 #[command(name = "mlf")] 11 22 #[command(about = "MLF (Matt's Lexicon Format) CLI tool", long_about = None)] ··· 47 58 #[arg(long, help = "Use flat file structure (e.g., app.bsky.post.json)")] 48 59 flat: bool, 49 60 }, 61 + Code { 62 + #[arg(short, long, help = "Generator to use (json, typescript, go, rust)")] 63 + generator: String, 64 + 65 + #[arg(short, long, help = "Input MLF files (glob patterns supported)")] 66 + input: Vec<String>, 67 + 68 + #[arg(short, long, help = "Output directory")] 69 + output: PathBuf, 70 + 71 + #[arg(long, help = "Use flat file structure (e.g., app.bsky.post.ts)")] 72 + flat: bool, 73 + }, 50 74 } 51 75 52 76 fn main() { ··· 62 86 Commands::Generate { command } => match command { 63 87 GenerateCommands::Lexicon { input, output, flat } => { 64 88 generate::lexicon::run(input, output, flat).into_diagnostic() 89 + } 90 + GenerateCommands::Code { generator, input, output, flat } => { 91 + generate::code::run(generator, input, output, flat).into_diagnostic() 65 92 } 66 93 }, 67 94 };
+1 -1
tree-sitter-mlf/test.mlf
··· 7 7 }, 8 8 /// Creation timestamp 9 9 createdAt: Datetime, 10 - }; 10 + }