1use crate::conflict_detector::FileConflictInfo;
11use crate::conflict_resolver::{ConflictResolver, ConflictStrategy};
12use crate::error::GenerationError;
13use crate::models::GeneratedFile;
14use std::fs;
15use std::path::{Path, PathBuf};
16
17#[derive(Debug, Clone)]
19pub struct OutputWriterConfig {
20 pub dry_run: bool,
22 pub create_backups: bool,
24 pub format_code: bool,
26 pub conflict_strategy: ConflictStrategy,
28}
29
30impl Default for OutputWriterConfig {
31 fn default() -> Self {
32 Self {
33 dry_run: false,
34 create_backups: true,
35 format_code: true,
36 conflict_strategy: ConflictStrategy::Skip,
37 }
38 }
39}
40
41#[derive(Debug, Clone)]
43pub struct FileWriteResult {
44 pub path: PathBuf,
46 pub written: bool,
48 pub backup_path: Option<PathBuf>,
50 pub action: String,
52 pub dry_run: bool,
54}
55
56#[derive(Debug, Clone)]
58pub struct WriteResult {
59 pub files: Vec<FileWriteResult>,
61 pub files_written: usize,
63 pub files_skipped: usize,
65 pub backups_created: usize,
67 pub dry_run: bool,
69 pub rollback_info: Option<RollbackInfo>,
71}
72
73#[derive(Debug, Clone)]
75pub struct RollbackInfo {
76 pub backups: Vec<(PathBuf, PathBuf)>, pub written_files: Vec<PathBuf>,
80}
81
82pub struct OutputWriter {
90 config: OutputWriterConfig,
91 conflict_resolver: ConflictResolver,
92}
93
94impl OutputWriter {
95 pub fn new() -> Self {
97 Self {
98 config: OutputWriterConfig::default(),
99 conflict_resolver: ConflictResolver::new(),
100 }
101 }
102
103 pub fn with_config(config: OutputWriterConfig) -> Self {
105 Self {
106 config,
107 conflict_resolver: ConflictResolver::new(),
108 }
109 }
110
111 pub fn write(
128 &self,
129 files: &[GeneratedFile],
130 target_dir: &Path,
131 conflicts: &[FileConflictInfo],
132 ) -> Result<WriteResult, GenerationError> {
133 let mut file_results = Vec::new();
134 let mut backups = Vec::new();
135 let mut written_files = Vec::new();
136 let mut files_written = 0;
137 let mut files_skipped = 0;
138 let mut backups_created = 0;
139
140 for file in files {
142 let file_path = target_dir.join(&file.path);
143
144 if let Some(parent) = file_path.parent() {
146 if !parent.exists() && !self.config.dry_run {
147 fs::create_dir_all(parent).map_err(|e| {
148 GenerationError::WriteFailed(format!(
149 "Failed to create directory {}: {}",
150 parent.display(),
151 e
152 ))
153 })?;
154 }
155 }
156
157 let conflict = conflicts.iter().find(|c| c.path == file_path);
159
160 let result = if let Some(conflict) = conflict {
161 self.handle_conflict(&file_path, conflict, &file.content, &mut backups)?
163 } else {
164 self.write_file(&file_path, &file.content, &mut backups)?
166 };
167
168 if result.written {
169 files_written += 1;
170 written_files.push(file_path.clone());
171 } else {
172 files_skipped += 1;
173 }
174
175 if result.backup_path.is_some() {
176 backups_created += 1;
177 }
178
179 file_results.push(result);
180 }
181
182 if self.config.dry_run {
184 return Ok(WriteResult {
185 files: file_results,
186 files_written: 0,
187 files_skipped: files.len(),
188 backups_created: 0,
189 dry_run: true,
190 rollback_info: None,
191 });
192 }
193
194 if files_written > 0 && files_written < files.len() {
196 self.rollback(&backups, &written_files)?;
197 return Err(GenerationError::WriteFailed(
198 "Partial write detected, rolled back all changes".to_string(),
199 ));
200 }
201
202 let rollback_info = if !backups.is_empty() {
203 Some(RollbackInfo {
204 backups: backups.clone(),
205 written_files: written_files.clone(),
206 })
207 } else {
208 None
209 };
210
211 Ok(WriteResult {
212 files: file_results,
213 files_written,
214 files_skipped,
215 backups_created,
216 dry_run: false,
217 rollback_info,
218 })
219 }
220
221 fn write_file(
231 &self,
232 file_path: &Path,
233 content: &str,
234 backups: &mut Vec<(PathBuf, PathBuf)>,
235 ) -> Result<FileWriteResult, GenerationError> {
236 let backup_path = if file_path.exists() && self.config.create_backups {
238 let backup = self.create_backup(file_path)?;
239 backups.push((file_path.to_path_buf(), backup.clone()));
240 Some(backup)
241 } else {
242 None
243 };
244
245 if !self.config.dry_run {
247 let content_to_write = if self.config.format_code {
248 self.format_code(content, file_path)?
249 } else {
250 content.to_string()
251 };
252
253 fs::write(file_path, content_to_write).map_err(|e| {
254 GenerationError::WriteFailed(format!(
255 "Failed to write {}: {}",
256 file_path.display(),
257 e
258 ))
259 })?;
260 }
261
262 let has_backup = backup_path.is_some();
263 Ok(FileWriteResult {
264 path: file_path.to_path_buf(),
265 written: true,
266 backup_path,
267 action: if has_backup {
268 "Written (backup created)".to_string()
269 } else {
270 "Written".to_string()
271 },
272 dry_run: self.config.dry_run,
273 })
274 }
275
276 fn handle_conflict(
287 &self,
288 file_path: &Path,
289 conflict: &FileConflictInfo,
290 new_content: &str,
291 backups: &mut Vec<(PathBuf, PathBuf)>,
292 ) -> Result<FileWriteResult, GenerationError> {
293 let resolution =
295 self.conflict_resolver
296 .resolve(conflict, self.config.conflict_strategy, new_content)?;
297
298 if let Some(backup_path) = &resolution.backup_path {
300 backups.push((file_path.to_path_buf(), PathBuf::from(backup_path)));
301 }
302
303 Ok(FileWriteResult {
304 path: file_path.to_path_buf(),
305 written: resolution.written,
306 backup_path: resolution.backup_path.map(PathBuf::from),
307 action: resolution.action,
308 dry_run: self.config.dry_run,
309 })
310 }
311
312 fn create_backup(&self, file_path: &Path) -> Result<PathBuf, GenerationError> {
320 let backup_path = format!("{}.bak", file_path.display());
321 let backup_path_obj = PathBuf::from(&backup_path);
322
323 if !self.config.dry_run {
324 let content = fs::read_to_string(file_path).map_err(|e| {
325 GenerationError::WriteFailed(format!("Failed to read file for backup: {}", e))
326 })?;
327
328 fs::write(&backup_path_obj, content).map_err(|e| {
329 GenerationError::WriteFailed(format!("Failed to create backup: {}", e))
330 })?;
331 }
332
333 Ok(backup_path_obj)
334 }
335
336 fn format_code(&self, content: &str, _file_path: &Path) -> Result<String, GenerationError> {
345 Ok(content.to_string())
348 }
349
350 fn rollback(
359 &self,
360 backups: &[(PathBuf, PathBuf)],
361 written_files: &[PathBuf],
362 ) -> Result<(), GenerationError> {
363 for (original_path, backup_path) in backups {
365 if backup_path.exists() {
366 let backup_content = fs::read_to_string(backup_path).map_err(|e| {
367 GenerationError::RollbackFailed(format!("Failed to read backup: {}", e))
368 })?;
369
370 fs::write(original_path, backup_content).map_err(|e| {
371 GenerationError::RollbackFailed(format!("Failed to restore backup: {}", e))
372 })?;
373
374 fs::remove_file(backup_path).map_err(|e| {
376 GenerationError::RollbackFailed(format!("Failed to remove backup: {}", e))
377 })?;
378 }
379 }
380
381 for file_path in written_files {
383 if file_path.exists() {
384 fs::remove_file(file_path).map_err(|e| {
385 GenerationError::RollbackFailed(format!("Failed to remove file: {}", e))
386 })?;
387 }
388 }
389
390 Ok(())
391 }
392
393 pub fn preview(
403 &self,
404 files: &[GeneratedFile],
405 target_dir: &Path,
406 conflicts: &[FileConflictInfo],
407 ) -> Result<WriteResult, GenerationError> {
408 let mut config = self.config.clone();
409 config.dry_run = true;
410
411 let writer = OutputWriter::with_config(config);
412 writer.write(files, target_dir, conflicts)
413 }
414
415 pub fn summarize_result(&self, result: &WriteResult) -> String {
423 format!(
424 "Files written: {}, Files skipped: {}, Backups created: {}{}",
425 result.files_written,
426 result.files_skipped,
427 result.backups_created,
428 if result.dry_run { " (dry-run)" } else { "" }
429 )
430 }
431}
432
433impl Default for OutputWriter {
434 fn default() -> Self {
435 Self::new()
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442 use tempfile::TempDir;
443
444 #[test]
445 fn test_create_output_writer() {
446 let _writer = OutputWriter::new();
447 }
448
449 #[test]
450 fn test_write_single_file() {
451 let temp_dir = TempDir::new().unwrap();
452 let writer = OutputWriter::new();
453
454 let files = vec![GeneratedFile {
455 path: "src/main.rs".to_string(),
456 content: "fn main() {}".to_string(),
457 language: "rust".to_string(),
458 }];
459
460 let result = writer.write(&files, temp_dir.path(), &[]).unwrap();
461
462 assert_eq!(result.files_written, 1);
463 assert_eq!(result.files_skipped, 0);
464 assert!(temp_dir.path().join("src/main.rs").exists());
465 }
466
467 #[test]
468 fn test_write_multiple_files() {
469 let temp_dir = TempDir::new().unwrap();
470 let writer = OutputWriter::new();
471
472 let files = vec![
473 GeneratedFile {
474 path: "src/main.rs".to_string(),
475 content: "fn main() {}".to_string(),
476 language: "rust".to_string(),
477 },
478 GeneratedFile {
479 path: "src/lib.rs".to_string(),
480 content: "pub fn lib() {}".to_string(),
481 language: "rust".to_string(),
482 },
483 ];
484
485 let result = writer.write(&files, temp_dir.path(), &[]).unwrap();
486
487 assert_eq!(result.files_written, 2);
488 assert_eq!(result.files_skipped, 0);
489 assert!(temp_dir.path().join("src/main.rs").exists());
490 assert!(temp_dir.path().join("src/lib.rs").exists());
491 }
492
493 #[test]
494 fn test_dry_run_mode() {
495 let temp_dir = TempDir::new().unwrap();
496 let config = OutputWriterConfig {
497 dry_run: true,
498 ..Default::default()
499 };
500 let writer = OutputWriter::with_config(config);
501
502 let files = vec![GeneratedFile {
503 path: "src/main.rs".to_string(),
504 content: "fn main() {}".to_string(),
505 language: "rust".to_string(),
506 }];
507
508 let result = writer.write(&files, temp_dir.path(), &[]).unwrap();
509
510 assert!(result.dry_run);
511 assert_eq!(result.files_written, 0);
512 assert_eq!(result.files_skipped, 1);
513 assert!(!temp_dir.path().join("src/main.rs").exists());
514 }
515
516 #[test]
517 fn test_create_backup() {
518 let temp_dir = TempDir::new().unwrap();
519 let writer = OutputWriter::new();
520
521 let file_path = temp_dir.path().join("existing.rs");
523 fs::write(&file_path, "old content").unwrap();
524
525 let files = vec![GeneratedFile {
526 path: "existing.rs".to_string(),
527 content: "new content".to_string(),
528 language: "rust".to_string(),
529 }];
530
531 let result = writer.write(&files, temp_dir.path(), &[]).unwrap();
532
533 assert_eq!(result.files_written, 1);
534 assert_eq!(result.backups_created, 1);
535
536 let backup_path = temp_dir.path().join("existing.rs.bak");
538 assert!(backup_path.exists());
539
540 let backup_content = fs::read_to_string(&backup_path).unwrap();
541 assert_eq!(backup_content, "old content");
542
543 let new_content = fs::read_to_string(&file_path).unwrap();
545 assert_eq!(new_content, "new content");
546 }
547
548 #[test]
549 fn test_conflict_skip_strategy() {
550 let temp_dir = TempDir::new().unwrap();
551 let config = OutputWriterConfig {
552 conflict_strategy: ConflictStrategy::Skip,
553 ..Default::default()
554 };
555 let writer = OutputWriter::with_config(config);
556
557 let file_path = temp_dir.path().join("existing.rs");
559 fs::write(&file_path, "old content").unwrap();
560
561 let files = vec![GeneratedFile {
562 path: "existing.rs".to_string(),
563 content: "new content".to_string(),
564 language: "rust".to_string(),
565 }];
566
567 let conflict = FileConflictInfo {
569 path: file_path.clone(),
570 old_content: "old content".to_string(),
571 new_content: "new content".to_string(),
572 diff: crate::conflict_detector::FileDiff {
573 added_lines: vec![],
574 removed_lines: vec![],
575 modified_lines: vec![],
576 total_changes: 0,
577 },
578 };
579
580 let result = writer.write(&files, temp_dir.path(), &[conflict]).unwrap();
581
582 assert_eq!(result.files_written, 0);
583 assert_eq!(result.files_skipped, 1);
584
585 let content = fs::read_to_string(&file_path).unwrap();
587 assert_eq!(content, "old content");
588 }
589
590 #[test]
591 fn test_conflict_overwrite_strategy() {
592 let temp_dir = TempDir::new().unwrap();
593 let config = OutputWriterConfig {
594 conflict_strategy: ConflictStrategy::Overwrite,
595 ..Default::default()
596 };
597 let writer = OutputWriter::with_config(config);
598
599 let file_path = temp_dir.path().join("existing.rs");
601 fs::write(&file_path, "old content").unwrap();
602
603 let files = vec![GeneratedFile {
604 path: "existing.rs".to_string(),
605 content: "new content".to_string(),
606 language: "rust".to_string(),
607 }];
608
609 let conflict = FileConflictInfo {
611 path: file_path.clone(),
612 old_content: "old content".to_string(),
613 new_content: "new content".to_string(),
614 diff: crate::conflict_detector::FileDiff {
615 added_lines: vec![],
616 removed_lines: vec![],
617 modified_lines: vec![],
618 total_changes: 0,
619 },
620 };
621
622 let result = writer.write(&files, temp_dir.path(), &[conflict]).unwrap();
623
624 assert_eq!(result.files_written, 1);
625 assert_eq!(result.backups_created, 1);
626
627 let content = fs::read_to_string(&file_path).unwrap();
629 assert_eq!(content, "new content");
630
631 let backup_path = temp_dir.path().join("existing.rs.bak");
633 assert!(backup_path.exists());
634 }
635
636 #[test]
637 fn test_preview_mode() {
638 let temp_dir = TempDir::new().unwrap();
639 let writer = OutputWriter::new();
640
641 let files = vec![GeneratedFile {
642 path: "src/main.rs".to_string(),
643 content: "fn main() {}".to_string(),
644 language: "rust".to_string(),
645 }];
646
647 let result = writer.preview(&files, temp_dir.path(), &[]).unwrap();
648
649 assert!(result.dry_run);
650 assert_eq!(result.files_written, 0);
651 assert!(!temp_dir.path().join("src/main.rs").exists());
652 }
653
654 #[test]
655 fn test_summarize_result() {
656 let writer = OutputWriter::new();
657 let result = WriteResult {
658 files: vec![],
659 files_written: 5,
660 files_skipped: 2,
661 backups_created: 3,
662 dry_run: false,
663 rollback_info: None,
664 };
665
666 let summary = writer.summarize_result(&result);
667 assert!(summary.contains("5"));
668 assert!(summary.contains("2"));
669 assert!(summary.contains("3"));
670 }
671
672 #[test]
673 fn test_create_nested_directories() {
674 let temp_dir = TempDir::new().unwrap();
675 let writer = OutputWriter::new();
676
677 let files = vec![GeneratedFile {
678 path: "src/nested/deep/main.rs".to_string(),
679 content: "fn main() {}".to_string(),
680 language: "rust".to_string(),
681 }];
682
683 let result = writer.write(&files, temp_dir.path(), &[]).unwrap();
684
685 assert_eq!(result.files_written, 1);
686 assert!(temp_dir.path().join("src/nested/deep/main.rs").exists());
687 }
688}