dampen_cli/commands/add/
mod.rs

1//! Add command for scaffolding new UI windows.
2//!
3//! This module provides the `dampen add --ui <window_name>` command that generates
4//! UI window files (`.rs` and `.dampen`) based on templates.
5//!
6//! # Overview
7//!
8//! The `add` command scaffolds new UI windows for Dampen applications by:
9//! - Generating a Rust module with model, handlers, and AppState
10//! - Creating a corresponding `.dampen` XML file with basic UI layout
11//! - Validating window names and output paths
12//! - Preventing accidental file overwrites
13//!
14//! # Usage
15//!
16//! ## Basic Usage
17//!
18//! Create a new window in the default location (`src/ui/`):
19//!
20//! ```bash
21//! dampen add --ui settings
22//! ```
23//!
24//! This generates:
25//! - `src/ui/settings.rs` - Rust module with Model and handlers
26//! - `src/ui/settings.dampen` - XML UI definition
27//!
28//! ## Custom Output Directory
29//!
30//! Specify a custom output directory with `--path`:
31//!
32//! ```bash
33//! dampen add --ui order_form --path "src/ui/orders"
34//! ```
35//!
36//! This generates files in `src/ui/orders/`:
37//! - `src/ui/orders/order_form.rs`
38//! - `src/ui/orders/order_form.dampen`
39//!
40//! ## Window Name Conventions
41//!
42//! Window names are automatically converted to proper case:
43//! - Input: `UserProfile` → Files: `user_profile.rs`, `user_profile.dampen`
44//! - Input: `settings` → Files: `settings.rs`, `settings.dampen`
45//!
46//! # Generated Code Structure
47//!
48//! The generated Rust module includes:
49//! - `Model` struct with `#[derive(UiModel)]` for data binding
50//! - `create_app_state()` function that returns configured `AppState<Model>`
51//! - `create_handler_registry()` with sample event handlers
52//! - Auto-loading via `#[dampen_ui]` macro
53//!
54//! The generated XML includes:
55//! - Basic column layout with text and button widgets
56//! - Data binding example (`{message}`)
57//! - Event handler hookup (`on_click="on_action"`)
58//!
59//! # After Generation
60//!
61//! 1. Add the module to `src/ui/mod.rs`:
62//!    ```rust,ignore
63//!    pub mod settings;
64//!    ```
65//!
66//! 2. Validate the XML:
67//!    ```bash
68//!    dampen check
69//!    ```
70//!
71//! 3. Use in your application:
72//!    ```rust,ignore
73//!    use ui::settings;
74//!    let state = settings::create_app_state();
75//!    ```
76//!
77//! # Error Handling
78//!
79//! The command validates:
80//! - Project context (must be a Dampen project with `dampen-core` dependency)
81//! - Window name (must be valid Rust identifier, not a reserved keyword)
82//! - Output path (must be relative, within project bounds)
83//! - File conflicts (prevents overwriting existing files)
84//!
85//! # Examples
86//!
87//! ```bash
88//! # Create a settings window
89//! dampen add --ui settings
90//!
91//! # Create an admin dashboard in a subdirectory
92//! dampen add --ui dashboard --path "src/ui/admin"
93//!
94//! # Create an order form
95//! dampen add --ui OrderForm
96//! # → Generates: order_form.rs, order_form.dampen
97//! ```
98
99use clap::Args;
100
101pub mod errors;
102pub mod generation;
103pub mod integration;
104pub mod templates;
105pub mod validation;
106pub mod view_switching;
107
108// Export error types (Phase 2 complete)
109pub use errors::{GenerationError, PathError, ProjectError, ValidationError};
110
111// Export template types (Phase 2 complete)
112pub use templates::{TemplateKind, WindowNameVariants, WindowTemplate};
113
114// Export validation types (Phase 3-4 complete)
115pub use validation::{ProjectInfo, TargetPath, WindowName};
116
117// Export generation types (Phase 5)
118pub use generation::{GeneratedFiles, generate_window_files};
119
120// Types will be exported as they're implemented in later phases
121// pub use validation::{TargetPath};
122
123/// Arguments for the `dampen add` command.
124///
125/// # Examples
126///
127/// ```bash
128/// # Add a window in default location (src/ui/)
129/// dampen add --ui settings
130///
131/// # Add a window in custom location
132/// dampen add --ui admin_panel --path "src/ui/admin"
133/// ```
134///
135/// # Fields
136///
137/// - `ui`: Window name (converted to snake_case for filenames)
138/// - `path`: Custom output directory (relative to project root)
139#[derive(Debug, Args)]
140pub struct AddArgs {
141    /// Add a new UI window
142    ///
143    /// The window name will be converted to snake_case for filenames.
144    ///
145    /// Examples:
146    ///   settings       → settings.rs, settings.dampen
147    ///   UserProfile    → user_profile.rs, user_profile.dampen
148    ///   admin-panel    → admin_panel.rs, admin_panel.dampen
149    #[arg(long)]
150    pub ui: Option<String>,
151
152    /// Custom output directory path (relative to project root)
153    ///
154    /// If not provided, defaults to "src/ui/"
155    ///
156    /// Examples:
157    ///   --path "src/ui/admin"      → Files in src/ui/admin/
158    ///   --path "ui/orders"         → Files in ui/orders/
159    ///
160    /// Security:
161    ///   - Must be relative (absolute paths rejected)
162    ///   - Must be within project (cannot escape via ..)
163    #[arg(long)]
164    pub path: Option<String>,
165
166    /// Disable automatic integration (do not update mod.rs)
167    ///
168    /// By default, the command automatically adds `pub mod <window_name>;`
169    /// to the appropriate mod.rs file. Use this flag to disable automatic
170    /// integration and handle module registration manually.
171    ///
172    /// Example:
173    ///   dampen add --ui settings --no-integrate
174    #[arg(long)]
175    pub no_integrate: bool,
176}
177
178/// Execute the add command.
179///
180/// This generates UI window files based on validated inputs.
181///
182/// # Process
183///
184/// 1. **Detect project**: Validates this is a Dampen project
185/// 2. **Validate name**: Checks window name is valid identifier
186/// 3. **Resolve path**: Determines output directory (default or custom)
187/// 4. **Generate files**: Creates .rs and .dampen files from templates
188/// 5. **Report success**: Shows file paths and next steps
189///
190/// # Errors
191///
192/// Returns `Err(String)` if:
193/// - Not in a Dampen project (no `dampen-core` in Cargo.toml)
194/// - Window name is invalid (empty, starts with number, reserved keyword)
195/// - Output path is invalid (absolute, escapes project)
196/// - Files already exist (prevents overwriting)
197/// - I/O errors occur during file creation
198///
199/// # Examples
200///
201/// ```no_run
202/// use dampen_cli::commands::add::{AddArgs, execute};
203///
204/// let args = AddArgs {
205///     ui: Some("settings".to_string()),
206///     path: None,
207///     no_integrate: false,
208/// };
209///
210/// match execute(&args) {
211///     Ok(()) => println!("Window created successfully"),
212///     Err(e) => eprintln!("Error: {}", e),
213/// }
214/// ```
215pub fn execute(args: &AddArgs) -> Result<(), String> {
216    // T073: Detect and validate project
217    let project_info = ProjectInfo::detect().map_err(|e| e.to_string())?;
218
219    if !project_info.is_dampen {
220        return Err(
221            "Error: Not a Dampen project (dampen-core dependency not found)\nhelp: Add dampen-core to your Cargo.toml, or run 'dampen new' to create a new project"
222                .to_string(),
223        );
224    }
225
226    // T074: Validate window name
227    let window_name_str = args
228        .ui
229        .as_ref()
230        .ok_or_else(|| "Error: Missing window name\nhelp: Use --ui <name>".to_string())?;
231
232    let window_name = WindowName::new(window_name_str).map_err(|e| e.to_string())?;
233
234    // T075: Resolve target path with validation (Phase 6)
235    let target_path =
236        TargetPath::resolve(&project_info.root, args.path.as_deref()).map_err(|e| e.to_string())?;
237
238    // T076: Generate files
239    let enable_integration = !args.no_integrate;
240    let generated = generate_window_files(&target_path, &window_name, enable_integration)
241        .map_err(|e| e.to_string())?;
242
243    // T077: Print success message
244    println!("{}", generated.success_message());
245
246    // T079: Return success
247    Ok(())
248}