anvil_engine/
generator.rs

1use std::path::{Path, PathBuf};
2#[cfg(unix)]
3use std::os::unix::fs::PermissionsExt;
4use tokio::fs;
5use tokio::io::AsyncWriteExt;
6
7use crate::engine::{ProcessedTemplate, ProcessedFile};
8use crate::error::{EngineError, EngineResult};
9
10pub struct FileGenerator {
11    output_dir: PathBuf,
12    dry_run: bool,
13}
14
15pub type ProgressCallback = Box<dyn Fn(usize, usize, &str) + Send + Sync>;
16
17#[derive(Debug)]
18pub struct GenerationResult {
19    pub files_created: usize,
20    pub directories_created: usize,
21    pub bytes_written: u64,
22    pub output_directory: PathBuf,
23}
24
25impl FileGenerator {
26    pub fn new(output_dir: impl Into<PathBuf>) -> Self {
27        Self {
28            output_dir: output_dir.into(),
29            dry_run: false,
30        }
31    }
32
33    pub fn new_dry_run(output_dir: impl Into<PathBuf>) -> Self {
34        Self {
35            output_dir: output_dir.into(),
36            dry_run: true,
37        }
38    }
39
40    pub async fn generate_files(
41        &self,
42        template: ProcessedTemplate,
43        progress_callback: Option<ProgressCallback>,
44    ) -> EngineResult<GenerationResult> {
45        let total_files = template.files.len();
46        let mut files_created = 0;
47        let mut directories_created = 0;
48        let mut bytes_written = 0u64;
49
50        if !self.dry_run {
51            fs::create_dir_all(&self.output_dir)
52                .await
53                .map_err(|e| EngineError::file_error(&self.output_dir, e))?;
54        }
55
56        for (index, file) in template.files.into_iter().enumerate() {
57            let (file_created, dirs_created, bytes) = Self::write_single_file(&self.output_dir, file, self.dry_run).await?;
58            
59            if file_created {
60                files_created += 1;
61            }
62            directories_created += dirs_created;
63            bytes_written += bytes;
64            
65            if let Some(callback) = &progress_callback {
66                callback(index + 1, total_files, "Processing files");
67            }
68        }
69
70        Ok(GenerationResult {
71            files_created,
72            directories_created,
73            bytes_written,
74            output_directory: self.output_dir.clone(),
75        })
76    }
77
78    async fn write_single_file(
79        output_dir: &Path,
80        file: ProcessedFile,
81        dry_run: bool,
82    ) -> EngineResult<(bool, usize, u64)> {
83        let full_path = output_dir.join(&file.output_path);
84        let mut directories_created = 0;
85
86        if let Some(parent) = full_path.parent() {
87            if !dry_run {
88                if !parent.exists() {
89                    fs::create_dir_all(parent)
90                        .await
91                        .map_err(|e| EngineError::file_error(parent, e))?;
92                    directories_created = Self::count_directories_in_path(parent, output_dir);
93                }
94            }
95        }
96
97        let bytes_written = file.content.len() as u64;
98
99        if !dry_run {
100            let mut file_handle = fs::File::create(&full_path)
101                .await
102                .map_err(|e| EngineError::file_error(&full_path, e))?;
103            
104            file_handle.write_all(file.content.as_bytes())
105                .await
106                .map_err(|e| EngineError::file_error(&full_path, e))?;
107            
108            file_handle.flush()
109                .await
110                .map_err(|e| EngineError::file_error(&full_path, e))?;
111
112            if file.executable {
113                Self::make_executable(&full_path).await?;
114            }
115        }
116
117        Ok((true, directories_created, bytes_written))
118    }
119
120    #[cfg(unix)]
121    async fn make_executable(path: &Path) -> EngineResult<()> {
122        let metadata = fs::metadata(path)
123            .await
124            .map_err(|e| EngineError::file_error(path, e))?;
125        
126        let mut permissions = metadata.permissions();
127        let mode = permissions.mode();
128        // Set user, group, and other execute bits (0o111) while preserving other permission bits
129        permissions.set_mode(mode | 0o111);
130        
131        fs::set_permissions(path, permissions)
132            .await
133            .map_err(|e| EngineError::file_error(path, e))?;
134        
135        Ok(())
136    }
137
138    #[cfg(not(unix))]
139    async fn make_executable(_path: &Path) -> EngineResult<()> {
140        Ok(())
141    }
142
143    fn count_directories_in_path(created_path: &Path, base_path: &Path) -> usize {
144        created_path
145            .strip_prefix(base_path)
146            .map(|relative| relative.components().count())
147            .unwrap_or(0)
148    }
149
150    pub async fn check_output_directory(&self) -> EngineResult<DirectoryStatus> {
151        if !self.output_dir.exists() {
152            return Ok(DirectoryStatus::DoesNotExist);
153        }
154
155        let mut entries = fs::read_dir(&self.output_dir)
156            .await
157            .map_err(|e| EngineError::file_error(&self.output_dir, e))?;
158
159        if entries.next_entry().await
160            .map_err(|e| EngineError::file_error(&self.output_dir, e))?
161            .is_some() {
162            Ok(DirectoryStatus::ExistsWithContent)
163        } else {
164            Ok(DirectoryStatus::ExistsEmpty)
165        }
166    }
167
168    pub async fn clean_output_directory(&self) -> EngineResult<()> {
169        if self.dry_run {
170            return Ok(());
171        }
172
173        if self.output_dir.exists() {
174            fs::remove_dir_all(&self.output_dir)
175                .await
176                .map_err(|e| EngineError::file_error(&self.output_dir, e))?;
177        }
178
179        fs::create_dir_all(&self.output_dir)
180            .await
181            .map_err(|e| EngineError::file_error(&self.output_dir, e))?;
182
183        Ok(())
184    }
185
186    pub fn output_directory(&self) -> &Path {
187        &self.output_dir
188    }
189
190    pub fn is_dry_run(&self) -> bool {
191        self.dry_run
192    }
193}
194
195#[derive(Debug, PartialEq, Eq)]
196pub enum DirectoryStatus {
197    DoesNotExist,
198    ExistsEmpty,
199    ExistsWithContent,
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use crate::engine::{ProcessedTemplate, ProcessedFile};
206    use tempfile::TempDir;
207    use std::path::PathBuf;
208
209    fn create_test_processed_template() -> ProcessedTemplate {
210        ProcessedTemplate {
211            files: vec![
212                ProcessedFile {
213                    output_path: PathBuf::from("main.rs"),
214                    content: "fn main() { println!(\"Hello, world!\"); }".to_string(),
215                    executable: false,
216                },
217                ProcessedFile {
218                    output_path: PathBuf::from("src/lib.rs"),
219                    content: "// Library code".to_string(),
220                    executable: false,
221                },
222                ProcessedFile {
223                    output_path: PathBuf::from("scripts/build.sh"),
224                    content: "#!/bin/bash\necho 'Building...'".to_string(),
225                    executable: true,
226                },
227            ],
228        }
229    }
230
231    #[tokio::test]
232    async fn test_file_generation() {
233        let temp_dir = TempDir::new().unwrap();
234        let output_dir = temp_dir.path().join("output");
235        
236        let generator = FileGenerator::new(&output_dir);
237        let template = create_test_processed_template();
238        
239        let result = generator.generate_files(template, None).await.unwrap();
240        
241        assert_eq!(result.files_created, 3);
242        assert!(result.bytes_written > 0);
243        assert_eq!(result.output_directory, output_dir);
244        
245        assert!(output_dir.join("main.rs").exists());
246        assert!(output_dir.join("src/lib.rs").exists());
247        assert!(output_dir.join("scripts/build.sh").exists());
248        
249        let main_content = fs::read_to_string(output_dir.join("main.rs")).await.unwrap();
250        assert!(main_content.contains("Hello, world!"));
251    }
252
253    #[tokio::test]
254    async fn test_dry_run() {
255        let temp_dir = TempDir::new().unwrap();
256        let output_dir = temp_dir.path().join("output");
257        
258        let generator = FileGenerator::new_dry_run(&output_dir);
259        let template = create_test_processed_template();
260        
261        let result = generator.generate_files(template, None).await.unwrap();
262        
263        assert_eq!(result.files_created, 3);
264        assert!(result.bytes_written > 0);
265        
266        assert!(!output_dir.exists());
267    }
268
269    #[tokio::test]
270    async fn test_directory_status_check() {
271        let temp_dir = TempDir::new().unwrap();
272        let output_dir = temp_dir.path().join("output");
273        
274        let generator = FileGenerator::new(&output_dir);
275        
276        let status = generator.check_output_directory().await.unwrap();
277        assert_eq!(status, DirectoryStatus::DoesNotExist);
278        
279        fs::create_dir_all(&output_dir).await.unwrap();
280        let status = generator.check_output_directory().await.unwrap();
281        assert_eq!(status, DirectoryStatus::ExistsEmpty);
282        
283        fs::write(output_dir.join("test.txt"), "content").await.unwrap();
284        let status = generator.check_output_directory().await.unwrap();
285        assert_eq!(status, DirectoryStatus::ExistsWithContent);
286    }
287
288    #[tokio::test]
289    async fn test_clean_output_directory() {
290        let temp_dir = TempDir::new().unwrap();
291        let output_dir = temp_dir.path().join("output");
292        
293        fs::create_dir_all(&output_dir).await.unwrap();
294        fs::write(output_dir.join("test.txt"), "content").await.unwrap();
295        
296        let generator = FileGenerator::new(&output_dir);
297        
298        let status = generator.check_output_directory().await.unwrap();
299        assert_eq!(status, DirectoryStatus::ExistsWithContent);
300        
301        generator.clean_output_directory().await.unwrap();
302        
303        let status = generator.check_output_directory().await.unwrap();
304        assert_eq!(status, DirectoryStatus::ExistsEmpty);
305    }
306
307    #[tokio::test]
308    async fn test_progress_callback() {
309        let temp_dir = TempDir::new().unwrap();
310        let output_dir = temp_dir.path().join("output");
311        
312        let generator = FileGenerator::new(&output_dir);
313        let template = create_test_processed_template();
314        
315        let progress_counter = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
316        let counter_clone = progress_counter.clone();
317        
318        let progress_callback: ProgressCallback = Box::new(move |current, total, _msg| {
319            counter_clone.store(current, std::sync::atomic::Ordering::Relaxed);
320            assert!(current <= total);
321        });
322        
323        let result = generator.generate_files(template, Some(progress_callback)).await.unwrap();
324        
325        assert_eq!(result.files_created, 3);
326        assert_eq!(progress_counter.load(std::sync::atomic::Ordering::Relaxed), 3);
327    }
328}
329