Skip to main content

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!(
300            !content.contains("<?xml"),
301            "Should NOT have XML declaration"
302        );
303        assert!(
304            content.contains("<dampen version=\"1.1\" encoding=\"utf-8\">"),
305            "Should have correct root element"
306        );
307        assert!(content.contains("<column"), "Should have column layout");
308        assert!(content.contains("Settings"), "Should contain window title");
309    }
310
311    #[test]
312    fn test_generate_files_rejects_existing_rs() {
313        // T101: Test that existing .rs file prevents generation
314        let temp = TempDir::new().unwrap();
315        let project_root = temp.path();
316        let target_dir = project_root.join("src/ui");
317        fs::create_dir_all(&target_dir).unwrap();
318
319        // Create existing .rs file
320        let existing_file = target_dir.join("settings.rs");
321        fs::write(&existing_file, "existing content").unwrap();
322
323        let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
324        let window_name = WindowName::new("settings").unwrap();
325        let result = generate_window_files(&target_path, &window_name, false);
326
327        assert!(result.is_err());
328        match result {
329            Err(GenerationError::FileExists {
330                window_name: name,
331                path,
332            }) => {
333                assert_eq!(name, "settings");
334                assert_eq!(path, existing_file);
335            }
336            _ => panic!("Expected FileExists error"),
337        }
338    }
339
340    #[test]
341    fn test_generate_files_rejects_existing_dampen() {
342        // T102: Test that existing .dampen file prevents generation
343        let temp = TempDir::new().unwrap();
344        let project_root = temp.path();
345        let target_dir = project_root.join("src/ui");
346        fs::create_dir_all(&target_dir).unwrap();
347
348        // Create existing .dampen file
349        let existing_file = target_dir.join("dashboard.dampen");
350        fs::write(&existing_file, "existing xml content").unwrap();
351
352        let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
353        let window_name = WindowName::new("dashboard").unwrap();
354        let result = generate_window_files(&target_path, &window_name, false);
355
356        assert!(result.is_err());
357        match result {
358            Err(GenerationError::FileExists {
359                window_name: name,
360                path,
361            }) => {
362                assert_eq!(name, "dashboard");
363                assert_eq!(path, existing_file);
364            }
365            _ => panic!("Expected FileExists error"),
366        }
367    }
368
369    #[test]
370    fn test_generate_files_rejects_partial_conflict() {
371        // T103: Test that partial conflict (only .rs exists) still prevents generation
372        let temp = TempDir::new().unwrap();
373        let project_root = temp.path();
374        let target_dir = project_root.join("src/ui");
375        fs::create_dir_all(&target_dir).unwrap();
376
377        // Create only .rs file (no .dampen file)
378        let existing_rs = target_dir.join("profile.rs");
379        fs::write(&existing_rs, "existing rust content").unwrap();
380
381        let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
382        let window_name = WindowName::new("profile").unwrap();
383        let result = generate_window_files(&target_path, &window_name, false);
384
385        // Should still reject because .rs exists
386        assert!(result.is_err());
387        match result {
388            Err(GenerationError::FileExists {
389                window_name: name,
390                path,
391            }) => {
392                assert_eq!(name, "profile");
393                assert_eq!(path, existing_rs);
394            }
395            _ => panic!("Expected FileExists error for partial conflict"),
396        }
397
398        // Clean up and try with only .dampen file
399        fs::remove_file(&existing_rs).unwrap();
400        let existing_dampen = target_dir.join("profile.dampen");
401        fs::write(&existing_dampen, "existing xml").unwrap();
402
403        let result2 = generate_window_files(&target_path, &window_name, false);
404
405        // Should also reject because .dampen exists
406        assert!(result2.is_err());
407        match result2 {
408            Err(GenerationError::FileExists {
409                window_name: name,
410                path,
411            }) => {
412                assert_eq!(name, "profile");
413                assert_eq!(path, existing_dampen);
414            }
415            _ => panic!("Expected FileExists error for .dampen conflict"),
416        }
417    }
418
419    #[test]
420    fn test_success_message_format() {
421        let temp = TempDir::new().unwrap();
422        let project_root = temp.path();
423        let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
424        let window_name = WindowName::new("dashboard").unwrap();
425
426        let generated = GeneratedFiles {
427            rust_file: target_path.absolute.join("dashboard.rs"),
428            dampen_file: target_path.absolute.join("dashboard.dampen"),
429            window_name: window_name.clone(),
430            target_dir: target_path.absolute.clone(),
431            updated_mod_file: None,
432            view_switching_activated: false,
433        };
434
435        let message = generated.success_message();
436
437        assert!(message.contains("Created UI window 'dashboard'"));
438        assert!(message.contains("dashboard.rs"));
439        assert!(message.contains("dashboard.dampen"));
440        assert!(message.contains("pub mod dashboard"));
441        assert!(message.contains("Next steps"));
442    }
443
444    #[test]
445    fn test_success_message_with_integration() {
446        let temp = TempDir::new().unwrap();
447        let project_root = temp.path();
448        let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
449        let window_name = WindowName::new("settings").unwrap();
450
451        let generated = GeneratedFiles {
452            rust_file: target_path.absolute.join("settings.rs"),
453            dampen_file: target_path.absolute.join("settings.dampen"),
454            window_name: window_name.clone(),
455            target_dir: target_path.absolute.clone(),
456            updated_mod_file: Some(project_root.join("src/ui/mod.rs")),
457            view_switching_activated: false,
458        };
459
460        let message = generated.success_message();
461
462        assert!(message.contains("Created UI window 'settings'"));
463        assert!(message.contains("settings.rs"));
464        assert!(message.contains("settings.dampen"));
465        assert!(message.contains("Updated"), "Should mention mod.rs update");
466        assert!(message.contains("src/ui/mod.rs"), "Should show mod.rs path");
467        assert!(
468            !message.contains("pub mod settings"),
469            "Should NOT show manual mod instruction"
470        );
471        assert!(message.contains("Next steps"));
472    }
473
474    #[test]
475    fn test_generate_files_with_integration() {
476        let temp = TempDir::new().unwrap();
477        let project_root = temp.path();
478
479        // Create src/ui directory and empty mod.rs
480        let ui_dir = project_root.join("src/ui");
481        fs::create_dir_all(&ui_dir).unwrap();
482        fs::write(ui_dir.join("mod.rs"), "").unwrap();
483
484        let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
485        let window_name = WindowName::new("settings").unwrap();
486
487        let result = generate_window_files(&target_path, &window_name, true);
488
489        assert!(result.is_ok());
490        let generated = result.unwrap();
491
492        // Files should be created
493        assert!(generated.rust_file.exists());
494        assert!(generated.dampen_file.exists());
495
496        // mod.rs should be updated
497        assert!(generated.updated_mod_file.is_some());
498        let mod_file = generated.updated_mod_file.unwrap();
499        assert_eq!(mod_file, ui_dir.join("mod.rs"));
500
501        // Check mod.rs content
502        let mod_content = fs::read_to_string(&mod_file).unwrap();
503        assert!(mod_content.contains("pub mod settings;"));
504    }
505
506    #[test]
507    fn test_generate_files_integration_disabled() {
508        let temp = TempDir::new().unwrap();
509        let project_root = temp.path();
510
511        // Create src/ui directory and empty mod.rs
512        let ui_dir = project_root.join("src/ui");
513        fs::create_dir_all(&ui_dir).unwrap();
514        fs::write(ui_dir.join("mod.rs"), "").unwrap();
515
516        let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
517        let window_name = WindowName::new("settings").unwrap();
518
519        let result = generate_window_files(&target_path, &window_name, false);
520
521        assert!(result.is_ok());
522        let generated = result.unwrap();
523
524        // Files should be created
525        assert!(generated.rust_file.exists());
526        assert!(generated.dampen_file.exists());
527
528        // mod.rs should NOT be updated
529        assert!(generated.updated_mod_file.is_none());
530
531        // Check mod.rs is still empty
532        let mod_content = fs::read_to_string(ui_dir.join("mod.rs")).unwrap();
533        assert!(mod_content.is_empty());
534    }
535
536    #[test]
537    fn test_view_switching_activated_on_second_window() {
538        let temp = TempDir::new().unwrap();
539        let project_root = temp.path();
540
541        // Create src/ui directory with mod.rs and first window
542        let ui_dir = project_root.join("src/ui");
543        fs::create_dir_all(&ui_dir).unwrap();
544        fs::write(ui_dir.join("mod.rs"), "pub mod window;\n").unwrap();
545        fs::write(ui_dir.join("window.rs"), "// First window").unwrap();
546
547        // Create a minimal main.rs with commented view switching
548        let src_dir = project_root.join("src");
549        fs::create_dir_all(&src_dir).unwrap();
550        let main_content = r#"
551enum Message {
552    // SwitchToView(CurrentView),
553    Handler(HandlerMessage),
554}
555
556#[dampen_app(
557    ui_dir = "src/ui",
558    message_type = "Message",
559    // switch_view_variant = "SwitchToView",
560)]
561struct App;
562"#;
563        fs::write(src_dir.join("main.rs"), main_content).unwrap();
564
565        // Add second window
566        let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
567        let window_name = WindowName::new("settings").unwrap();
568
569        let result = generate_window_files(&target_path, &window_name, true);
570
571        assert!(result.is_ok());
572        let generated = result.unwrap();
573
574        // View switching should be activated
575        assert!(generated.view_switching_activated);
576
577        // Check main.rs was modified
578        let main_content = fs::read_to_string(src_dir.join("main.rs")).unwrap();
579        assert!(main_content.contains("SwitchToView(CurrentView),"));
580        assert!(!main_content.contains("// SwitchToView(CurrentView),"));
581        assert!(main_content.contains(r#"switch_view_variant = "SwitchToView","#));
582        assert!(!main_content.contains(r#"// switch_view_variant"#));
583    }
584
585    #[test]
586    fn test_view_switching_not_activated_on_first_window() {
587        let temp = TempDir::new().unwrap();
588        let project_root = temp.path();
589
590        // Create src/ui directory with only mod.rs (no existing windows)
591        let ui_dir = project_root.join("src/ui");
592        fs::create_dir_all(&ui_dir).unwrap();
593        fs::write(ui_dir.join("mod.rs"), "").unwrap();
594
595        // Add first window
596        let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
597        let window_name = WindowName::new("window").unwrap();
598
599        let result = generate_window_files(&target_path, &window_name, true);
600
601        assert!(result.is_ok());
602        let generated = result.unwrap();
603
604        // View switching should NOT be activated (only one window)
605        assert!(!generated.view_switching_activated);
606    }
607}