A human-friendly DSL for ATProto Lexicons
0
fork

Configure Feed

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

at main 371 lines 12 kB view raw
1use miette::Diagnostic; 2use mlf_codegen::plugin::GeneratorContext; 3use std::path::{Path, PathBuf}; 4use thiserror::Error; 5 6#[derive(Error, Debug, Diagnostic)] 7pub 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("Generator '{name}' not found")] 25 #[diagnostic(code(mlf::generate::generator_not_found))] 26 #[help("Available generators: {}", available.join(", "))] 27 GeneratorNotFound { 28 name: String, 29 available: Vec<String>, 30 }, 31 32 #[error("Code generation failed: {0}")] 33 #[diagnostic(code(mlf::generate::generation_failed))] 34 #[allow(dead_code)] 35 GenerationFailed(String), 36} 37 38pub fn run( 39 generator_name: Option<String>, 40 input_paths: Vec<PathBuf>, 41 output_dir: Option<PathBuf>, 42 root: Option<PathBuf>, 43 flat: bool, 44) -> Result<(), GenerateError> { 45 let current_dir = std::env::current_dir().map_err(|source| GenerateError::WriteOutput { 46 path: "current directory".to_string(), 47 source, 48 })?; 49 50 // Load mlf.toml if available 51 let project_root = crate::config::find_project_root(&current_dir).ok(); 52 let config = project_root 53 .as_ref() 54 .and_then(|root| { 55 let config_path = root.join("mlf.toml"); 56 crate::config::MlfConfig::load(&config_path).ok() 57 }); 58 59 // Determine generator name 60 let generator_name = if let Some(explicit) = generator_name { 61 explicit 62 } else if let Some(cfg) = &config { 63 // Find first non-lexicon, non-mlf output in mlf.toml 64 cfg.output 65 .iter() 66 .find(|o| o.r#type != "lexicon" && o.r#type != "mlf") 67 .map(|o| o.r#type.clone()) 68 .ok_or_else(|| GenerateError::GeneratorNotFound { 69 name: "any".to_string(), 70 available: vec!["No code generator outputs configured in mlf.toml. Either add an output configuration or provide --generator flag.".to_string()], 71 })? 72 } else { 73 return Err(GenerateError::GeneratorNotFound { 74 name: "any".to_string(), 75 available: vec!["No mlf.toml found and no --generator flag provided. Either create a mlf.toml or provide --generator flag.".to_string()], 76 }); 77 }; 78 79 // Find the generator 80 let generators = mlf_codegen::plugin::generators(); 81 let generator = generators 82 .iter() 83 .find(|g| g.name() == generator_name) 84 .ok_or_else(|| { 85 let available: Vec<String> = generators.iter().map(|g| g.name().to_string()).collect(); 86 GenerateError::GeneratorNotFound { 87 name: generator_name.clone(), 88 available, 89 } 90 })?; 91 92 println!("Using generator: {} ({})", generator.name(), generator.description()); 93 println!("Output extension: {}\n", generator.file_extension()); 94 95 // Determine output directory 96 let output_dir = if let Some(explicit) = output_dir { 97 explicit 98 } else if let Some(cfg) = &config { 99 // Find output matching the generator type 100 cfg.output 101 .iter() 102 .find(|o| o.r#type == generator_name) 103 .map(|o| PathBuf::from(&o.directory)) 104 .ok_or_else(|| GenerateError::WriteOutput { 105 path: "mlf.toml".to_string(), 106 source: std::io::Error::new( 107 std::io::ErrorKind::NotFound, 108 format!("No output configured for generator '{}' in mlf.toml", generator_name) 109 ), 110 })? 111 } else { 112 return Err(GenerateError::WriteOutput { 113 path: "mlf.toml".to_string(), 114 source: std::io::Error::new( 115 std::io::ErrorKind::NotFound, 116 "No mlf.toml found and no --output flag provided" 117 ), 118 }); 119 }; 120 121 // Determine root directory 122 let root_dir = if let Some(explicit) = root { 123 explicit 124 } else if let Some(cfg) = &config { 125 project_root.as_ref().unwrap().join(&cfg.source.directory) 126 } else { 127 current_dir.clone() 128 }; 129 130 // Determine input paths 131 let input_paths = if input_paths.is_empty() { 132 if let Some(cfg) = &config { 133 vec![project_root.as_ref().unwrap().join(&cfg.source.directory)] 134 } else { 135 return Err(GenerateError::WriteOutput { 136 path: "input".to_string(), 137 source: std::io::Error::new( 138 std::io::ErrorKind::NotFound, 139 "No input files specified and no mlf.toml found" 140 ), 141 }); 142 } 143 } else { 144 input_paths 145 }; 146 147 // Collect input files 148 let mut file_paths = Vec::new(); 149 for path in input_paths { 150 if path.is_dir() { 151 file_paths.extend(collect_mlf_files(&path)?); 152 } else if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("mlf") { 153 file_paths.push(path); 154 } 155 } 156 157 std::fs::create_dir_all(&output_dir).map_err(|source| GenerateError::WriteOutput { 158 path: output_dir.display().to_string(), 159 source, 160 })?; 161 162 let mut errors = Vec::new(); 163 let mut success_count = 0; 164 165 for file_path in file_paths { 166 let source = match std::fs::read_to_string(&file_path) { 167 Ok(s) => s, 168 Err(e) => { 169 errors.push(( 170 file_path.display().to_string(), 171 format!("Failed to read file: {}", e), 172 )); 173 continue; 174 } 175 }; 176 177 let lexicon = match mlf_lang::parse_lexicon(&source) { 178 Ok(lex) => lex, 179 Err(e) => { 180 errors.push((file_path.display().to_string(), format!("{:?}", e))); 181 continue; 182 } 183 }; 184 185 let namespace = match extract_namespace(&file_path, &root_dir) { 186 Ok(ns) => ns, 187 Err(e) => { 188 errors.push(( 189 file_path.display().to_string(), 190 format!("Failed to extract namespace: {}", e), 191 )); 192 continue; 193 } 194 }; 195 196 // Create workspace with standard library and .mlf cache 197 let mlf_cache_dir = crate::config::find_project_root(&std::env::current_dir().unwrap()) 198 .ok() 199 .map(|root| crate::config::get_mlf_cache_dir(&root)); 200 201 let mut workspace = match crate::workspace_ext::workspace_with_std_and_cache(mlf_cache_dir.as_deref()) { 202 Ok(ws) => ws, 203 Err(e) => { 204 errors.push(( 205 file_path.display().to_string(), 206 format!("Failed to load workspace: {}", e), 207 )); 208 continue; 209 } 210 }; 211 212 // Add the module to the workspace 213 if let Err(e) = workspace.add_module(namespace.clone(), lexicon.clone()) { 214 errors.push(( 215 file_path.display().to_string(), 216 format!("Failed to add module: {:?}", e), 217 )); 218 continue; 219 } 220 221 // Resolve types 222 if let Err(e) = workspace.resolve() { 223 errors.push(( 224 file_path.display().to_string(), 225 format!("Type resolution error: {:?}", e), 226 )); 227 continue; 228 } 229 230 // Generate code using the selected generator 231 let ctx = GeneratorContext { 232 namespace: &namespace, 233 lexicon: &lexicon, 234 workspace: &workspace, 235 }; 236 237 let generated_code = match generator.generate(&ctx) { 238 Ok(code) => code, 239 Err(e) => { 240 errors.push((file_path.display().to_string(), format!("Generation error: {}", e))); 241 continue; 242 } 243 }; 244 245 // Determine output path 246 let output_path = if flat { 247 let filename = format!("{}{}", namespace, generator.file_extension()); 248 output_dir.join(filename) 249 } else { 250 let mut path = output_dir.clone(); 251 for segment in namespace.split('.') { 252 path.push(segment); 253 } 254 if let Err(e) = std::fs::create_dir_all(&path.parent().unwrap()) { 255 errors.push(( 256 file_path.display().to_string(), 257 format!("Failed to create directory: {}", e), 258 )); 259 continue; 260 } 261 path.set_extension(generator.file_extension().trim_start_matches('.')); 262 path 263 }; 264 265 // Write generated code 266 if let Err(source) = std::fs::write(&output_path, generated_code) { 267 errors.push(( 268 output_path.display().to_string(), 269 format!("Failed to write file: {}", source), 270 )); 271 continue; 272 } 273 274 println!("Generated: {}", output_path.display()); 275 success_count += 1; 276 } 277 278 if !errors.is_empty() { 279 eprintln!( 280 "\n{} file(s) generated successfully, {} error(s) encountered:\n", 281 success_count, 282 errors.len() 283 ); 284 for (path, error) in &errors { 285 eprintln!(" {} - {}", path, error); 286 } 287 eprintln!(); 288 return Err(GenerateError::ParseLexicon { 289 path: "multiple files".to_string(), 290 help: Some(format!("{} errors total", errors.len())), 291 }); 292 } 293 294 println!("\nSuccessfully generated {} file(s)", success_count); 295 Ok(()) 296} 297 298/// Collect all .mlf files recursively from a directory 299fn collect_mlf_files(dir: &Path) -> Result<Vec<PathBuf>, GenerateError> { 300 let mut files = Vec::new(); 301 302 for entry in std::fs::read_dir(dir).map_err(|source| GenerateError::WriteOutput { 303 path: dir.display().to_string(), 304 source, 305 })? { 306 let entry = entry.map_err(|source| GenerateError::WriteOutput { 307 path: dir.display().to_string(), 308 source, 309 })?; 310 311 let path = entry.path(); 312 313 if path.is_dir() { 314 files.extend(collect_mlf_files(&path)?); 315 } else if path.extension().and_then(|s| s.to_str()) == Some("mlf") { 316 files.push(path); 317 } 318 } 319 320 Ok(files) 321} 322 323fn extract_namespace(file_path: &Path, root_dir: &Path) -> Result<String, std::io::Error> { 324 // Canonicalize both paths for comparison 325 let file_canonical = file_path.canonicalize()?; 326 let root_canonical = root_dir.canonicalize()?; 327 328 // Get the relative path from root to file 329 let relative_path = file_canonical 330 .strip_prefix(&root_canonical) 331 .map_err(|_| { 332 std::io::Error::new( 333 std::io::ErrorKind::Other, 334 format!( 335 "File path {} is not under root directory {}", 336 file_path.display(), 337 root_dir.display() 338 ), 339 ) 340 })?; 341 342 // Convert path components to namespace parts 343 let mut namespace_parts = Vec::new(); 344 345 for component in relative_path.components() { 346 match component { 347 std::path::Component::Normal(os_str) => { 348 if let Some(s) = os_str.to_str() { 349 namespace_parts.push(s); 350 } 351 } 352 _ => continue, 353 } 354 } 355 356 // Remove .mlf extension from the last component if present 357 if let Some(last) = namespace_parts.last_mut() { 358 if let Some(stem) = last.strip_suffix(".mlf") { 359 *last = stem; 360 } 361 } 362 363 if namespace_parts.is_empty() { 364 return Err(std::io::Error::new( 365 std::io::ErrorKind::Other, 366 format!("Could not extract namespace from path: {}", file_path.display()), 367 )); 368 } 369 370 Ok(namespace_parts.join(".")) 371}