forked from
stavola.xyz/mlf
A human-friendly DSL for ATProto Lexicons
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(¤t_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}