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 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