1use crate::codegen::{Codegen, FileMode};
9use crate::formatter::Formatter;
10use crate::{CodegenError, Result};
11use std::path::{Path, PathBuf};
12
13#[derive(Debug, Clone)]
15pub struct GeneratedFile {
16 pub path: PathBuf,
18 pub content: String,
20 pub mode: FileMode,
22 pub language: String,
24}
25
26#[derive(Debug, Clone)]
28pub struct GenerateOptions {
29 pub output_dir: PathBuf,
31 pub check: bool,
33 pub diff: bool,
35}
36
37impl Default for GenerateOptions {
38 fn default() -> Self {
39 Self {
40 output_dir: PathBuf::from("."),
41 check: false,
42 diff: false,
43 }
44 }
45}
46
47#[derive(Debug)]
49pub struct Generator {
50 codegen: Codegen,
51 formatter: Formatter,
52}
53
54impl Generator {
55 #[must_use]
57 pub fn new(codegen: Codegen) -> Self {
58 Self {
59 codegen,
60 formatter: Formatter::new(),
61 }
62 }
63
64 pub fn generate(&self, options: &GenerateOptions) -> Result<Vec<GeneratedFile>> {
70 let mut generated_files = Vec::new();
71
72 for (file_path, file_def) in self.codegen.files() {
73 let output_path = options.output_dir.join(file_path);
74
75 let formatted_content =
77 self.formatter
78 .format(&file_def.content, &file_def.language, &file_def.format)?;
79
80 let generated = GeneratedFile {
81 path: output_path.clone(),
82 content: formatted_content.clone(),
83 mode: file_def.mode,
84 language: file_def.language.clone(),
85 };
86
87 match file_def.mode {
89 FileMode::Managed => {
90 if options.check {
91 self.check_file(&output_path, &formatted_content)?;
92 } else {
93 self.write_file(&output_path, &formatted_content)?;
94 }
95 }
96 FileMode::Scaffold => {
97 if output_path.exists() {
98 tracing::info!("Skipping {} (scaffold mode, file exists)", file_path);
99 } else if options.check {
100 return Err(CodegenError::Generation(format!(
101 "Missing scaffold file: {file_path}"
102 )));
103 } else {
104 self.write_file(&output_path, &formatted_content)?;
105 }
106 }
107 }
108
109 generated_files.push(generated);
110 }
111
112 Ok(generated_files)
113 }
114
115 #[allow(clippy::unused_self)] fn write_file(&self, path: &Path, content: &str) -> Result<()> {
118 if let Some(parent) = path.parent() {
120 std::fs::create_dir_all(parent)?;
121 }
122
123 std::fs::write(path, content)?;
124 tracing::info!("Generated: {}", path.display());
125
126 Ok(())
127 }
128
129 #[allow(clippy::unused_self)] fn check_file(&self, path: &Path, expected_content: &str) -> Result<()> {
132 if !path.exists() {
133 return Err(CodegenError::Generation(format!(
134 "Missing managed file: {}",
135 path.display()
136 )));
137 }
138
139 let actual_content = std::fs::read_to_string(path)?;
140
141 if actual_content != expected_content {
142 return Err(CodegenError::Generation(format!(
143 "File would be modified: {}",
144 path.display()
145 )));
146 }
147
148 Ok(())
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::codegen::{CodegenData, FormatConfig, ProjectFileDefinition};
156 use std::collections::HashMap;
157 use tempfile::TempDir;
158
159 fn create_test_codegen() -> Codegen {
160 let mut files = HashMap::new();
161 files.insert(
162 "test.json".to_string(),
163 ProjectFileDefinition {
164 content: r#"{"name":"test"}"#.to_string(),
165 language: "json".to_string(),
166 mode: FileMode::Managed,
167 format: FormatConfig::default(),
168 gitignore: false,
169 },
170 );
171
172 let data = CodegenData {
173 files,
174 context: serde_json::Value::Null,
175 };
176
177 Codegen {
178 data,
179 source_path: PathBuf::from("test.cue"),
180 }
181 }
182
183 fn create_scaffold_codegen() -> Codegen {
184 let mut files = HashMap::new();
185 files.insert(
186 "scaffold.txt".to_string(),
187 ProjectFileDefinition {
188 content: "scaffold content".to_string(),
189 language: "text".to_string(),
190 mode: FileMode::Scaffold,
191 format: FormatConfig::default(),
192 gitignore: false,
193 },
194 );
195
196 let data = CodegenData {
197 files,
198 context: serde_json::Value::Null,
199 };
200
201 Codegen {
202 data,
203 source_path: PathBuf::from("scaffold.cue"),
204 }
205 }
206
207 #[test]
208 fn test_generate_options_default() {
209 let options = GenerateOptions::default();
210 assert_eq!(options.output_dir, PathBuf::from("."));
211 assert!(!options.check);
212 assert!(!options.diff);
213 }
214
215 #[test]
216 fn test_generated_file_clone() {
217 let file = GeneratedFile {
218 path: PathBuf::from("test.rs"),
219 content: "fn main() {}".to_string(),
220 mode: FileMode::Managed,
221 language: "rust".to_string(),
222 };
223 let cloned = file.clone();
224 assert_eq!(cloned.path, file.path);
225 assert_eq!(cloned.content, file.content);
226 assert_eq!(cloned.mode, file.mode);
227 assert_eq!(cloned.language, file.language);
228 }
229
230 #[test]
231 fn test_generated_file_debug() {
232 let file = GeneratedFile {
233 path: PathBuf::from("test.rs"),
234 content: "fn main() {}".to_string(),
235 mode: FileMode::Managed,
236 language: "rust".to_string(),
237 };
238 let debug = format!("{file:?}");
239 assert!(debug.contains("test.rs"));
240 assert!(debug.contains("rust"));
241 }
242
243 #[test]
244 fn test_generator_new() {
245 let codegen = create_test_codegen();
246 let generator = Generator::new(codegen);
247 assert!(generator.codegen.files().contains_key("test.json"));
248 }
249
250 #[test]
251 fn test_generate_managed_file() {
252 let codegen = create_test_codegen();
253 let generator = Generator::new(codegen);
254
255 let temp_dir = TempDir::new().unwrap();
256 let options = GenerateOptions {
257 output_dir: temp_dir.path().to_path_buf(),
258 check: false,
259 diff: false,
260 };
261
262 let result = generator.generate(&options);
263 assert!(result.is_ok());
264
265 let generated = result.unwrap();
266 assert_eq!(generated.len(), 1);
267 assert_eq!(generated[0].mode, FileMode::Managed);
268
269 let file_path = temp_dir.path().join("test.json");
270 assert!(file_path.exists());
271 }
272
273 #[test]
274 fn test_generate_scaffold_creates_new_file() {
275 let codegen = create_scaffold_codegen();
276 let generator = Generator::new(codegen);
277
278 let temp_dir = TempDir::new().unwrap();
279 let options = GenerateOptions {
280 output_dir: temp_dir.path().to_path_buf(),
281 check: false,
282 diff: false,
283 };
284
285 let result = generator.generate(&options);
286 assert!(result.is_ok());
287
288 let file_path = temp_dir.path().join("scaffold.txt");
289 assert!(file_path.exists());
290 let content = std::fs::read_to_string(file_path).unwrap();
291 assert_eq!(content, "scaffold content");
292 }
293
294 #[test]
295 fn test_generate_scaffold_skips_existing_file() {
296 let codegen = create_scaffold_codegen();
297 let generator = Generator::new(codegen);
298
299 let temp_dir = TempDir::new().unwrap();
300 let file_path = temp_dir.path().join("scaffold.txt");
301 std::fs::write(&file_path, "existing content").unwrap();
302
303 let options = GenerateOptions {
304 output_dir: temp_dir.path().to_path_buf(),
305 check: false,
306 diff: false,
307 };
308
309 let result = generator.generate(&options);
310 assert!(result.is_ok());
311
312 let content = std::fs::read_to_string(file_path).unwrap();
314 assert_eq!(content, "existing content");
315 }
316
317 #[test]
318 fn test_generate_check_mode_missing_managed_file() {
319 let codegen = create_test_codegen();
320 let generator = Generator::new(codegen);
321
322 let temp_dir = TempDir::new().unwrap();
323 let options = GenerateOptions {
324 output_dir: temp_dir.path().to_path_buf(),
325 check: true,
326 diff: false,
327 };
328
329 let result = generator.generate(&options);
330 assert!(result.is_err());
331 let err = result.unwrap_err();
332 assert!(err.to_string().contains("Missing managed file"));
333 }
334
335 #[test]
336 fn test_generate_check_mode_file_would_be_modified() {
337 let codegen = create_test_codegen();
338 let generator = Generator::new(codegen);
339
340 let temp_dir = TempDir::new().unwrap();
341 let file_path = temp_dir.path().join("test.json");
342 std::fs::write(&file_path, "different content").unwrap();
343
344 let options = GenerateOptions {
345 output_dir: temp_dir.path().to_path_buf(),
346 check: true,
347 diff: false,
348 };
349
350 let result = generator.generate(&options);
351 assert!(result.is_err());
352 let err = result.unwrap_err();
353 assert!(err.to_string().contains("would be modified"));
354 }
355
356 #[test]
357 fn test_generate_check_mode_file_matches() {
358 let codegen = create_test_codegen();
359 let generator = Generator::new(codegen);
360
361 let temp_dir = TempDir::new().unwrap();
362
363 let options = GenerateOptions {
365 output_dir: temp_dir.path().to_path_buf(),
366 check: false,
367 diff: false,
368 };
369 generator.generate(&options).unwrap();
370
371 let check_options = GenerateOptions {
373 output_dir: temp_dir.path().to_path_buf(),
374 check: true,
375 diff: false,
376 };
377 let result = generator.generate(&check_options);
378 assert!(result.is_ok());
379 }
380
381 #[test]
382 fn test_generate_check_mode_missing_scaffold_file() {
383 let codegen = create_scaffold_codegen();
384 let generator = Generator::new(codegen);
385
386 let temp_dir = TempDir::new().unwrap();
387 let options = GenerateOptions {
388 output_dir: temp_dir.path().to_path_buf(),
389 check: true,
390 diff: false,
391 };
392
393 let result = generator.generate(&options);
394 assert!(result.is_err());
395 let err = result.unwrap_err();
396 assert!(err.to_string().contains("Missing scaffold file"));
397 }
398
399 #[test]
400 fn test_generate_creates_nested_directories() {
401 let mut files = HashMap::new();
402 files.insert(
403 "deep/nested/path/file.txt".to_string(),
404 ProjectFileDefinition {
405 content: "nested content".to_string(),
406 language: "text".to_string(),
407 mode: FileMode::Managed,
408 format: FormatConfig::default(),
409 gitignore: false,
410 },
411 );
412
413 let codegen = Codegen {
414 data: CodegenData {
415 files,
416 context: serde_json::Value::Null,
417 },
418 source_path: PathBuf::from("test.cue"),
419 };
420 let generator = Generator::new(codegen);
421
422 let temp_dir = TempDir::new().unwrap();
423 let options = GenerateOptions {
424 output_dir: temp_dir.path().to_path_buf(),
425 check: false,
426 diff: false,
427 };
428
429 let result = generator.generate(&options);
430 assert!(result.is_ok());
431
432 let file_path = temp_dir.path().join("deep/nested/path/file.txt");
433 assert!(file_path.exists());
434 let content = std::fs::read_to_string(file_path).unwrap();
435 assert_eq!(content, "nested content");
436 }
437
438 #[test]
439 fn test_generate_multiple_files() {
440 let mut files = HashMap::new();
441 files.insert(
442 "file1.txt".to_string(),
443 ProjectFileDefinition {
444 content: "content 1".to_string(),
445 language: "text".to_string(),
446 mode: FileMode::Managed,
447 format: FormatConfig::default(),
448 gitignore: false,
449 },
450 );
451 files.insert(
452 "file2.txt".to_string(),
453 ProjectFileDefinition {
454 content: "content 2".to_string(),
455 language: "text".to_string(),
456 mode: FileMode::Scaffold,
457 format: FormatConfig::default(),
458 gitignore: false,
459 },
460 );
461
462 let codegen = Codegen {
463 data: CodegenData {
464 files,
465 context: serde_json::Value::Null,
466 },
467 source_path: PathBuf::from("test.cue"),
468 };
469 let generator = Generator::new(codegen);
470
471 let temp_dir = TempDir::new().unwrap();
472 let options = GenerateOptions {
473 output_dir: temp_dir.path().to_path_buf(),
474 check: false,
475 diff: false,
476 };
477
478 let result = generator.generate(&options);
479 assert!(result.is_ok());
480
481 let generated = result.unwrap();
482 assert_eq!(generated.len(), 2);
483
484 assert!(temp_dir.path().join("file1.txt").exists());
485 assert!(temp_dir.path().join("file2.txt").exists());
486 }
487}