dampen_cli/commands/add/
generation.rs

1//! File generation logic for creating window files from templates.
2
3use crate::commands::add::errors::GenerationError;
4use crate::commands::add::integration::add_module_to_mod_rs;
5use crate::commands::add::templates::{TemplateKind, WindowTemplate};
6use crate::commands::add::validation::{TargetPath, WindowName};
7use crate::commands::add::view_switching::{activate_view_switching, detect_second_window};
8use std::fs;
9use std::path::PathBuf;
10
11/// Result of file generation
12#[derive(Debug, Clone)]
13pub struct GeneratedFiles {
14    /// Path to generated .rs file
15    pub rust_file: PathBuf,
16
17    /// Path to generated .dampen file
18    pub dampen_file: PathBuf,
19
20    /// Validated window name
21    pub window_name: WindowName,
22
23    /// Target directory where files were created
24    pub target_dir: PathBuf,
25
26    /// Path to mod.rs that was updated (if integration was performed)
27    pub updated_mod_file: Option<PathBuf>,
28
29    /// Whether main.rs was modified to enable view switching
30    pub view_switching_activated: bool,
31}
32
33impl GeneratedFiles {
34    /// Generate a success message showing what was created
35    pub fn success_message(&self) -> String {
36        let mut message = format!(
37            "✓ Created UI window '{}'\n  → {}\n  → {}",
38            self.window_name.snake,
39            self.rust_file.display(),
40            self.dampen_file.display()
41        );
42
43        // Show mod.rs update if integration was performed
44        if let Some(mod_file) = &self.updated_mod_file {
45            message.push_str(&format!("\n  → Updated {}", mod_file.display()));
46        }
47
48        // Show view switching activation
49        if self.view_switching_activated {
50            message.push_str("\n  → Activated multi-view in src/main.rs");
51        }
52
53        message.push_str("\n\nNext steps:");
54
55        // Only show manual mod.rs step if integration was NOT performed
56        if self.updated_mod_file.is_none() {
57            message.push_str(&format!(
58                "\n  1. Add `pub mod {};` to src/ui/mod.rs",
59                self.window_name.snake
60            ));
61            message.push_str("\n  2. Run `dampen check` to validate");
62            message.push_str("\n  3. Run your application to see the new window");
63        } else {
64            message.push_str("\n  1. Run `dampen check` to validate");
65            message.push_str("\n  2. Run your application to see the new window");
66        }
67
68        message
69    }
70}
71
72/// Generate window files (.rs and .dampen) from templates
73///
74/// # Arguments
75///
76/// * `target_path` - Validated target path with project context
77/// * `window_name` - Validated window name with case variants
78/// * `enable_integration` - Whether to automatically add module to mod.rs
79///
80/// # Returns
81///
82/// `GeneratedFiles` struct with paths to created files
83///
84/// # Errors
85///
86/// Returns `GenerationError` if:
87/// - Files already exist (prevents overwriting)
88/// - Directory creation fails
89/// - File write operations fail
90///
91/// # Note
92///
93/// Integration errors (mod.rs updates) are non-fatal and emitted as warnings
94pub fn generate_window_files(
95    target_path: &TargetPath,
96    window_name: &WindowName,
97    enable_integration: bool,
98) -> Result<GeneratedFiles, GenerationError> {
99    // 1. Check if files already exist (prevent overwriting)
100    let rust_file = target_path.file_path(&window_name.snake, "rs");
101    let dampen_file = target_path.file_path(&window_name.snake, "dampen");
102
103    if rust_file.exists() {
104        return Err(GenerationError::FileExists {
105            window_name: window_name.snake.clone(),
106            path: rust_file,
107        });
108    }
109
110    if dampen_file.exists() {
111        return Err(GenerationError::FileExists {
112            window_name: window_name.snake.clone(),
113            path: dampen_file,
114        });
115    }
116
117    // 2. Create target directory if it doesn't exist
118    fs::create_dir_all(&target_path.absolute).map_err(|e| GenerationError::DirectoryCreation {
119        path: target_path.absolute.clone(),
120        source: e,
121    })?;
122
123    // 3. Load and render templates
124    let rust_template = WindowTemplate::load(TemplateKind::RustModule);
125    let dampen_template = WindowTemplate::load(TemplateKind::DampenXml);
126
127    let variants = window_name.to_variants();
128    let rust_content = rust_template.render(&variants);
129    let dampen_content = dampen_template.render(&variants);
130
131    // 4. Write .rs file
132    fs::write(&rust_file, rust_content).map_err(|e| GenerationError::FileWrite {
133        path: rust_file.clone(),
134        source: e,
135    })?;
136
137    // 5. Write .dampen file (with cleanup on error)
138    if let Err(e) = fs::write(&dampen_file, dampen_content) {
139        // Cleanup: remove .rs file if .dampen write fails
140        let _ = fs::remove_file(&rust_file);
141        return Err(GenerationError::FileWrite {
142            path: dampen_file,
143            source: e,
144        });
145    }
146
147    // 6. Attempt mod.rs integration if enabled
148    let updated_mod_file = if enable_integration {
149        match add_module_to_mod_rs(
150            &target_path.project_root,
151            &target_path.absolute,
152            &window_name.snake,
153        ) {
154            Ok(mod_path) => {
155                eprintln!("✓ Updated {}", mod_path.display());
156                Some(mod_path)
157            }
158            Err(e) => {
159                eprintln!("⚠ Warning: Failed to update mod.rs: {}", e);
160                eprintln!(
161                    "  Please manually add `pub mod {};` to the appropriate mod.rs file",
162                    window_name.snake
163                );
164                None
165            }
166        }
167    } else {
168        None
169    };
170
171    // 7. Activate view switching if this is the second window
172    let view_switching_activated = if enable_integration {
173        match detect_second_window(&target_path.project_root) {
174            Ok(true) => {
175                // This is the second window, activate view switching
176                match activate_view_switching(&target_path.project_root) {
177                    Ok(()) => {
178                        eprintln!("✓ Activated multi-view in src/main.rs");
179                        true
180                    }
181                    Err(e) => {
182                        eprintln!("⚠ Warning: Failed to activate view switching: {}", e);
183                        eprintln!(
184                            "  You may need to manually enable multi-view support in src/main.rs"
185                        );
186                        false
187                    }
188                }
189            }
190            Ok(false) => false,
191            Err(e) => {
192                eprintln!("⚠ Warning: Could not detect second window: {}", e);
193                false
194            }
195        }
196    } else {
197        false
198    };
199
200    Ok(GeneratedFiles {
201        rust_file,
202        dampen_file,
203        window_name: window_name.clone(),
204        target_dir: target_path.absolute.clone(),
205        updated_mod_file,
206        view_switching_activated,
207    })
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use crate::commands::add::validation::{TargetPath, WindowName};
214    use std::fs;
215    use tempfile::TempDir;
216
217    #[test]
218    fn test_generate_files_default_path() {
219        let temp = TempDir::new().unwrap();
220        let project_root = temp.path();
221        let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
222        let window_name = WindowName::new("settings").unwrap();
223
224        let result = generate_window_files(&target_path, &window_name, false);
225
226        assert!(result.is_ok());
227        let generated = result.unwrap();
228        assert_eq!(generated.rust_file, project_root.join("src/ui/settings.rs"));
229        assert_eq!(
230            generated.dampen_file,
231            project_root.join("src/ui/settings.dampen")
232        );
233        assert!(generated.rust_file.exists());
234        assert!(generated.dampen_file.exists());
235    }
236
237    #[test]
238    fn test_generate_files_creates_directory() {
239        let temp = TempDir::new().unwrap();
240        let project_root = temp.path();
241        let target_path = TargetPath::resolve(project_root, Some("src/ui/admin")).unwrap();
242        let window_name = WindowName::new("dashboard").unwrap();
243
244        // Directory doesn't exist yet
245        assert!(!target_path.absolute.exists());
246
247        let result = generate_window_files(&target_path, &window_name, false);
248
249        assert!(result.is_ok());
250        assert!(target_path.absolute.exists());
251        assert!(target_path.absolute.join("dashboard.rs").exists());
252        assert!(target_path.absolute.join("dashboard.dampen").exists());
253    }
254
255    #[test]
256    fn test_generate_files_rs_content() {
257        let temp = TempDir::new().unwrap();
258        let project_root = temp.path();
259        let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
260        let window_name = WindowName::new("user_profile").unwrap();
261
262        let result = generate_window_files(&target_path, &window_name, false);
263
264        assert!(result.is_ok());
265        let content = fs::read_to_string(target_path.absolute.join("user_profile.rs")).unwrap();
266
267        // Check for key patterns in generated Rust file
268        assert!(
269            content.contains("user_profile"),
270            "Should contain module name"
271        );
272        assert!(
273            content.contains("pub struct Model"),
274            "Should contain Model struct"
275        );
276        assert!(
277            content.contains("#[dampen_ui"),
278            "Should contain dampen_ui attribute"
279        );
280        assert!(
281            content.contains("create_app_state"),
282            "Should contain create_app_state function"
283        );
284    }
285
286    #[test]
287    fn test_generate_files_dampen_content() {
288        let temp = TempDir::new().unwrap();
289        let project_root = temp.path();
290        let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
291        let window_name = WindowName::new("settings").unwrap();
292
293        let result = generate_window_files(&target_path, &window_name, false);
294
295        assert!(result.is_ok());
296        let content = fs::read_to_string(target_path.absolute.join("settings.dampen")).unwrap();
297
298        // Check for key patterns in generated XML file
299        assert!(content.contains("<?xml"), "Should have XML declaration");
300        assert!(content.contains("<column"), "Should have column layout");
301        assert!(content.contains("Settings"), "Should contain window title");
302    }
303
304    #[test]
305    fn test_generate_files_rejects_existing_rs() {
306        // T101: Test that existing .rs file prevents generation
307        let temp = TempDir::new().unwrap();
308        let project_root = temp.path();
309        let target_dir = project_root.join("src/ui");
310        fs::create_dir_all(&target_dir).unwrap();
311
312        // Create existing .rs file
313        let existing_file = target_dir.join("settings.rs");
314        fs::write(&existing_file, "existing content").unwrap();
315
316        let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
317        let window_name = WindowName::new("settings").unwrap();
318        let result = generate_window_files(&target_path, &window_name, false);
319
320        assert!(result.is_err());
321        match result {
322            Err(GenerationError::FileExists {
323                window_name: name,
324                path,
325            }) => {
326                assert_eq!(name, "settings");
327                assert_eq!(path, existing_file);
328            }
329            _ => panic!("Expected FileExists error"),
330        }
331    }
332
333    #[test]
334    fn test_generate_files_rejects_existing_dampen() {
335        // T102: Test that existing .dampen file prevents generation
336        let temp = TempDir::new().unwrap();
337        let project_root = temp.path();
338        let target_dir = project_root.join("src/ui");
339        fs::create_dir_all(&target_dir).unwrap();
340
341        // Create existing .dampen file
342        let existing_file = target_dir.join("dashboard.dampen");
343        fs::write(&existing_file, "existing xml content").unwrap();
344
345        let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
346        let window_name = WindowName::new("dashboard").unwrap();
347        let result = generate_window_files(&target_path, &window_name, false);
348
349        assert!(result.is_err());
350        match result {
351            Err(GenerationError::FileExists {
352                window_name: name,
353                path,
354            }) => {
355                assert_eq!(name, "dashboard");
356                assert_eq!(path, existing_file);
357            }
358            _ => panic!("Expected FileExists error"),
359        }
360    }
361
362    #[test]
363    fn test_generate_files_rejects_partial_conflict() {
364        // T103: Test that partial conflict (only .rs exists) still prevents generation
365        let temp = TempDir::new().unwrap();
366        let project_root = temp.path();
367        let target_dir = project_root.join("src/ui");
368        fs::create_dir_all(&target_dir).unwrap();
369
370        // Create only .rs file (no .dampen file)
371        let existing_rs = target_dir.join("profile.rs");
372        fs::write(&existing_rs, "existing rust content").unwrap();
373
374        let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
375        let window_name = WindowName::new("profile").unwrap();
376        let result = generate_window_files(&target_path, &window_name, false);
377
378        // Should still reject because .rs exists
379        assert!(result.is_err());
380        match result {
381            Err(GenerationError::FileExists {
382                window_name: name,
383                path,
384            }) => {
385                assert_eq!(name, "profile");
386                assert_eq!(path, existing_rs);
387            }
388            _ => panic!("Expected FileExists error for partial conflict"),
389        }
390
391        // Clean up and try with only .dampen file
392        fs::remove_file(&existing_rs).unwrap();
393        let existing_dampen = target_dir.join("profile.dampen");
394        fs::write(&existing_dampen, "existing xml").unwrap();
395
396        let result2 = generate_window_files(&target_path, &window_name, false);
397
398        // Should also reject because .dampen exists
399        assert!(result2.is_err());
400        match result2 {
401            Err(GenerationError::FileExists {
402                window_name: name,
403                path,
404            }) => {
405                assert_eq!(name, "profile");
406                assert_eq!(path, existing_dampen);
407            }
408            _ => panic!("Expected FileExists error for .dampen conflict"),
409        }
410    }
411
412    #[test]
413    fn test_success_message_format() {
414        let temp = TempDir::new().unwrap();
415        let project_root = temp.path();
416        let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
417        let window_name = WindowName::new("dashboard").unwrap();
418
419        let generated = GeneratedFiles {
420            rust_file: target_path.absolute.join("dashboard.rs"),
421            dampen_file: target_path.absolute.join("dashboard.dampen"),
422            window_name: window_name.clone(),
423            target_dir: target_path.absolute.clone(),
424            updated_mod_file: None,
425            view_switching_activated: false,
426        };
427
428        let message = generated.success_message();
429
430        assert!(message.contains("Created UI window 'dashboard'"));
431        assert!(message.contains("dashboard.rs"));
432        assert!(message.contains("dashboard.dampen"));
433        assert!(message.contains("pub mod dashboard"));
434        assert!(message.contains("Next steps"));
435    }
436
437    #[test]
438    fn test_success_message_with_integration() {
439        let temp = TempDir::new().unwrap();
440        let project_root = temp.path();
441        let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
442        let window_name = WindowName::new("settings").unwrap();
443
444        let generated = GeneratedFiles {
445            rust_file: target_path.absolute.join("settings.rs"),
446            dampen_file: target_path.absolute.join("settings.dampen"),
447            window_name: window_name.clone(),
448            target_dir: target_path.absolute.clone(),
449            updated_mod_file: Some(project_root.join("src/ui/mod.rs")),
450            view_switching_activated: false,
451        };
452
453        let message = generated.success_message();
454
455        assert!(message.contains("Created UI window 'settings'"));
456        assert!(message.contains("settings.rs"));
457        assert!(message.contains("settings.dampen"));
458        assert!(message.contains("Updated"), "Should mention mod.rs update");
459        assert!(message.contains("src/ui/mod.rs"), "Should show mod.rs path");
460        assert!(
461            !message.contains("pub mod settings"),
462            "Should NOT show manual mod instruction"
463        );
464        assert!(message.contains("Next steps"));
465    }
466
467    #[test]
468    fn test_generate_files_with_integration() {
469        let temp = TempDir::new().unwrap();
470        let project_root = temp.path();
471
472        // Create src/ui directory and empty mod.rs
473        let ui_dir = project_root.join("src/ui");
474        fs::create_dir_all(&ui_dir).unwrap();
475        fs::write(ui_dir.join("mod.rs"), "").unwrap();
476
477        let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
478        let window_name = WindowName::new("settings").unwrap();
479
480        let result = generate_window_files(&target_path, &window_name, true);
481
482        assert!(result.is_ok());
483        let generated = result.unwrap();
484
485        // Files should be created
486        assert!(generated.rust_file.exists());
487        assert!(generated.dampen_file.exists());
488
489        // mod.rs should be updated
490        assert!(generated.updated_mod_file.is_some());
491        let mod_file = generated.updated_mod_file.unwrap();
492        assert_eq!(mod_file, ui_dir.join("mod.rs"));
493
494        // Check mod.rs content
495        let mod_content = fs::read_to_string(&mod_file).unwrap();
496        assert!(mod_content.contains("pub mod settings;"));
497    }
498
499    #[test]
500    fn test_generate_files_integration_disabled() {
501        let temp = TempDir::new().unwrap();
502        let project_root = temp.path();
503
504        // Create src/ui directory and empty mod.rs
505        let ui_dir = project_root.join("src/ui");
506        fs::create_dir_all(&ui_dir).unwrap();
507        fs::write(ui_dir.join("mod.rs"), "").unwrap();
508
509        let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
510        let window_name = WindowName::new("settings").unwrap();
511
512        let result = generate_window_files(&target_path, &window_name, false);
513
514        assert!(result.is_ok());
515        let generated = result.unwrap();
516
517        // Files should be created
518        assert!(generated.rust_file.exists());
519        assert!(generated.dampen_file.exists());
520
521        // mod.rs should NOT be updated
522        assert!(generated.updated_mod_file.is_none());
523
524        // Check mod.rs is still empty
525        let mod_content = fs::read_to_string(ui_dir.join("mod.rs")).unwrap();
526        assert!(mod_content.is_empty());
527    }
528
529    #[test]
530    fn test_view_switching_activated_on_second_window() {
531        let temp = TempDir::new().unwrap();
532        let project_root = temp.path();
533
534        // Create src/ui directory with mod.rs and first window
535        let ui_dir = project_root.join("src/ui");
536        fs::create_dir_all(&ui_dir).unwrap();
537        fs::write(ui_dir.join("mod.rs"), "pub mod window;\n").unwrap();
538        fs::write(ui_dir.join("window.rs"), "// First window").unwrap();
539
540        // Create a minimal main.rs with commented view switching
541        let src_dir = project_root.join("src");
542        fs::create_dir_all(&src_dir).unwrap();
543        let main_content = r#"
544enum Message {
545    // SwitchToView(CurrentView),
546    Handler(HandlerMessage),
547}
548
549#[dampen_app(
550    ui_dir = "src/ui",
551    message_type = "Message",
552    // switch_view_variant = "SwitchToView",
553)]
554struct App;
555"#;
556        fs::write(src_dir.join("main.rs"), main_content).unwrap();
557
558        // Add second window
559        let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
560        let window_name = WindowName::new("settings").unwrap();
561
562        let result = generate_window_files(&target_path, &window_name, true);
563
564        assert!(result.is_ok());
565        let generated = result.unwrap();
566
567        // View switching should be activated
568        assert!(generated.view_switching_activated);
569
570        // Check main.rs was modified
571        let main_content = fs::read_to_string(src_dir.join("main.rs")).unwrap();
572        assert!(main_content.contains("SwitchToView(CurrentView),"));
573        assert!(!main_content.contains("// SwitchToView(CurrentView),"));
574        assert!(main_content.contains(r#"switch_view_variant = "SwitchToView","#));
575        assert!(!main_content.contains(r#"// switch_view_variant"#));
576    }
577
578    #[test]
579    fn test_view_switching_not_activated_on_first_window() {
580        let temp = TempDir::new().unwrap();
581        let project_root = temp.path();
582
583        // Create src/ui directory with only mod.rs (no existing windows)
584        let ui_dir = project_root.join("src/ui");
585        fs::create_dir_all(&ui_dir).unwrap();
586        fs::write(ui_dir.join("mod.rs"), "").unwrap();
587
588        // Add first window
589        let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
590        let window_name = WindowName::new("window").unwrap();
591
592        let result = generate_window_files(&target_path, &window_name, true);
593
594        assert!(result.is_ok());
595        let generated = result.unwrap();
596
597        // View switching should NOT be activated (only one window)
598        assert!(!generated.view_switching_activated);
599    }
600}