1use std::collections::HashSet;
2use std::fs;
3use std::path::{Path, PathBuf};
4use thiserror::Error;
5
6#[derive(Error, Debug)]
7pub enum OutputError {
8 #[error("IO error: {0}")]
9 Io(#[from] std::io::Error),
10 #[error("Invalid output path: {0}")]
11 InvalidPath(String),
12 #[error("Permission denied: {0}")]
13 PermissionDenied(String),
14}
15
16pub struct OutputManager {
17 output_dir: PathBuf,
18 managed_files: HashSet<String>,
19 backup_dir: Option<PathBuf>,
20}
21
22impl OutputManager {
23 pub fn new<P: AsRef<Path>>(output_dir: P) -> Self {
24 Self {
25 output_dir: output_dir.as_ref().to_path_buf(),
26 managed_files: HashSet::new(),
27 backup_dir: None,
28 }
29 }
30
31 pub fn with_backup<P: AsRef<Path>>(output_dir: P, backup_dir: Option<P>) -> Self {
32 Self {
33 output_dir: output_dir.as_ref().to_path_buf(),
34 managed_files: HashSet::new(),
35 backup_dir: backup_dir.map(|p| p.as_ref().to_path_buf()),
36 }
37 }
38
39 pub fn prepare_output_directory(&self) -> Result<(), OutputError> {
41 if !self.output_dir.exists() {
42 fs::create_dir_all(&self.output_dir).map_err(|e| {
43 OutputError::PermissionDenied(format!(
44 "Cannot create output directory {}: {}",
45 self.output_dir.display(),
46 e
47 ))
48 })?;
49 }
50
51 let test_file = self.output_dir.join(".write_test");
53 fs::write(&test_file, "test").map_err(|e| {
54 OutputError::PermissionDenied(format!(
55 "Cannot write to output directory {}: {}",
56 self.output_dir.display(),
57 e
58 ))
59 })?;
60 fs::remove_file(&test_file).ok(); Ok(())
63 }
64
65 pub fn register_managed_file(&mut self, filename: &str) {
67 self.managed_files.insert(filename.to_string());
68 }
69
70 pub fn cleanup_old_files(&self, current_files: &[String]) -> Result<Vec<String>, OutputError> {
72 let mut cleaned_files = Vec::new();
73
74 if !self.output_dir.exists() {
75 return Ok(cleaned_files);
76 }
77
78 let current_set: HashSet<String> = current_files.iter().cloned().collect();
79
80 let entries = fs::read_dir(&self.output_dir)?;
82
83 for entry in entries {
84 let entry = entry?;
85 let path = entry.path();
86
87 if path.is_file() {
88 if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
89 if self.is_generated_file(filename) && !current_set.contains(filename) {
91 self.backup_and_remove_file(&path)?;
92 cleaned_files.push(filename.to_string());
93 }
94 }
95 }
96 }
97
98 Ok(cleaned_files)
99 }
100
101 fn is_generated_file(&self, filename: &str) -> bool {
103 let generated_patterns = [
105 "types.ts",
106 "types.d.ts",
107 "commands.ts",
108 "commands.d.ts",
109 "schemas.ts",
110 "schemas.d.ts",
111 "index.ts",
112 "index.d.ts",
113 "models.ts",
114 "models.d.ts",
115 "bindings.ts",
116 "bindings.d.ts",
117 ];
118
119 generated_patterns
120 .iter()
121 .any(|pattern| filename == *pattern)
122 || filename.starts_with("generated_")
123 || filename.contains("_generated")
124 || self.managed_files.contains(filename)
125 }
126
127 fn backup_and_remove_file(&self, file_path: &Path) -> Result<(), OutputError> {
129 if let Some(backup_dir) = &self.backup_dir {
130 if !backup_dir.exists() {
131 fs::create_dir_all(backup_dir)?;
132 }
133
134 if let Some(filename) = file_path.file_name() {
135 let backup_path = backup_dir.join(format!(
136 "{}.backup.{}",
137 filename.to_string_lossy(),
138 chrono::Utc::now().timestamp()
139 ));
140 fs::copy(file_path, backup_path)?;
141 }
142 }
143
144 fs::remove_file(file_path)?;
145 Ok(())
146 }
147
148 pub fn write_file(&self, filename: &str, content: &str) -> Result<PathBuf, OutputError> {
150 let file_path = self.output_dir.join(filename);
151
152 if let Some(parent) = file_path.parent() {
154 if !parent.exists() {
155 fs::create_dir_all(parent)?;
156 }
157 }
158
159 let temp_path = file_path.with_extension("tmp");
161 fs::write(&temp_path, content)?;
162 fs::rename(&temp_path, &file_path)?;
163
164 Ok(file_path)
165 }
166
167 pub fn verify_output(&self, expected_files: &[String]) -> Result<Vec<String>, OutputError> {
169 let mut missing_files = Vec::new();
170
171 for expected in expected_files {
172 let file_path = self.output_dir.join(expected);
173 if !file_path.exists() {
174 missing_files.push(expected.clone());
175 }
176 }
177
178 Ok(missing_files)
179 }
180
181 pub fn get_generation_metadata(&self) -> Result<GenerationMetadata, OutputError> {
183 let mut metadata = GenerationMetadata {
184 output_directory: self.output_dir.clone(),
185 generated_at: chrono::Utc::now(),
186 files: Vec::new(),
187 total_size: 0,
188 };
189
190 if !self.output_dir.exists() {
191 return Ok(metadata);
192 }
193
194 let entries = fs::read_dir(&self.output_dir)?;
195
196 for entry in entries {
197 let entry = entry?;
198 let path = entry.path();
199
200 if path.is_file() {
201 if let Ok(metadata_entry) = entry.metadata() {
202 let size = metadata_entry.len();
203 metadata.total_size += size;
204
205 if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
206 metadata.files.push(FileMetadata {
207 name: filename.to_string(),
208 path: path.clone(),
209 size,
210 modified: metadata_entry.modified().ok(),
211 });
212 }
213 }
214 }
215 }
216
217 Ok(metadata)
218 }
219
220 pub fn finalize_generation(&mut self, generated_files: &[String]) -> Result<(), OutputError> {
222 self.prepare_output_directory()?;
223
224 for file in generated_files {
226 self.register_managed_file(file);
227 }
228
229 let cleaned = self.cleanup_old_files(generated_files)?;
231 if !cleaned.is_empty() {
232 eprintln!("Cleaned up {} old generated files", cleaned.len());
233 }
234
235 let missing = self.verify_output(generated_files)?;
237 if !missing.is_empty() {
238 return Err(OutputError::InvalidPath(format!(
239 "Missing generated files: {}",
240 missing.join(", ")
241 )));
242 }
243
244 Ok(())
245 }
246
247 pub fn create_summary_report(&self) -> Result<String, OutputError> {
249 let metadata = self.get_generation_metadata()?;
250
251 let mut report = String::new();
252 report.push_str("# TypeScript Generation Summary\n\n");
253 report.push_str(&format!(
254 "Generated at: {}\n",
255 metadata.generated_at.format("%Y-%m-%d %H:%M:%S UTC")
256 ));
257 report.push_str(&format!(
258 "Output directory: {}\n",
259 metadata.output_directory.display()
260 ));
261 report.push_str(&format!("Total files: {}\n", metadata.files.len()));
262 report.push_str(&format!("Total size: {} bytes\n\n", metadata.total_size));
263
264 report.push_str("## Generated Files\n\n");
265 for file in &metadata.files {
266 report.push_str(&format!("- **{}** ({} bytes)\n", file.name, file.size));
267 }
268
269 Ok(report)
270 }
271}
272
273#[derive(Debug)]
274pub struct GenerationMetadata {
275 pub output_directory: PathBuf,
276 pub generated_at: chrono::DateTime<chrono::Utc>,
277 pub files: Vec<FileMetadata>,
278 pub total_size: u64,
279}
280
281#[derive(Debug)]
282pub struct FileMetadata {
283 pub name: String,
284 pub path: PathBuf,
285 pub size: u64,
286 pub modified: Option<std::time::SystemTime>,
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292 use tempfile::TempDir;
293
294 #[test]
295 fn test_prepare_output_directory() {
296 let temp_dir = TempDir::new().unwrap();
297 let output_path = temp_dir.path().join("output");
298
299 let manager = OutputManager::new(&output_path);
300 manager.prepare_output_directory().unwrap();
301
302 assert!(output_path.exists());
303 assert!(output_path.is_dir());
304 }
305
306 #[test]
307 fn test_write_file() {
308 let temp_dir = TempDir::new().unwrap();
309 let manager = OutputManager::new(temp_dir.path());
310
311 manager.prepare_output_directory().unwrap();
312
313 let content = "export interface Test { name: string; }";
314 let file_path = manager.write_file("test.ts", content).unwrap();
315
316 assert!(file_path.exists());
317 let written_content = fs::read_to_string(&file_path).unwrap();
318 assert_eq!(written_content, content);
319 }
320
321 #[test]
322 fn test_is_generated_file() {
323 let temp_dir = TempDir::new().unwrap();
324 let manager = OutputManager::new(temp_dir.path());
325
326 assert!(manager.is_generated_file("types.ts"));
328 assert!(manager.is_generated_file("commands.ts"));
329 assert!(manager.is_generated_file("schemas.ts"));
330 assert!(manager.is_generated_file("index.ts"));
331
332 assert!(manager.is_generated_file("generated_models.ts"));
334 assert!(manager.is_generated_file("api_generated.ts"));
335
336 assert!(!manager.is_generated_file("user_code.ts"));
338 assert!(!manager.is_generated_file("main.ts"));
339 assert!(!manager.is_generated_file("app.vue"));
340 }
341
342 #[test]
343 fn test_cleanup_old_files() {
344 let temp_dir = TempDir::new().unwrap();
345 let manager = OutputManager::new(temp_dir.path());
346
347 manager.prepare_output_directory().unwrap();
348
349 fs::write(temp_dir.path().join("types.ts"), "old content").unwrap();
351 fs::write(temp_dir.path().join("commands.ts"), "old content").unwrap();
352 fs::write(temp_dir.path().join("user_file.ts"), "user content").unwrap();
353
354 let current_files = vec!["types.ts".to_string()];
356
357 let cleaned = manager.cleanup_old_files(¤t_files).unwrap();
358
359 assert_eq!(cleaned.len(), 1);
361 assert!(cleaned.contains(&"commands.ts".to_string()));
362
363 assert!(temp_dir.path().join("types.ts").exists());
364 assert!(!temp_dir.path().join("commands.ts").exists());
365 assert!(temp_dir.path().join("user_file.ts").exists());
366 }
367
368 #[test]
369 fn test_verify_output() {
370 let temp_dir = TempDir::new().unwrap();
371 let manager = OutputManager::new(temp_dir.path());
372
373 manager.prepare_output_directory().unwrap();
374
375 fs::write(temp_dir.path().join("types.ts"), "content").unwrap();
377
378 let expected = vec!["types.ts".to_string(), "commands.ts".to_string()];
379 let missing = manager.verify_output(&expected).unwrap();
380
381 assert_eq!(missing.len(), 1);
382 assert!(missing.contains(&"commands.ts".to_string()));
383 }
384
385 #[test]
386 fn test_generation_metadata() {
387 let temp_dir = TempDir::new().unwrap();
388 let manager = OutputManager::new(temp_dir.path());
389
390 manager.prepare_output_directory().unwrap();
391
392 fs::write(temp_dir.path().join("types.ts"), "interface Test {}").unwrap();
394 fs::write(
395 temp_dir.path().join("commands.ts"),
396 "export function test() {}",
397 )
398 .unwrap();
399
400 let metadata = manager.get_generation_metadata().unwrap();
401
402 assert_eq!(metadata.files.len(), 2);
403 assert!(metadata.total_size > 0);
404 assert_eq!(metadata.output_directory, temp_dir.path());
405
406 let types_file = metadata
408 .files
409 .iter()
410 .find(|f| f.name == "types.ts")
411 .unwrap();
412 assert_eq!(types_file.size, 17); }
414}