dampen_cli/commands/
build.rs

1#![allow(clippy::print_stderr, clippy::print_stdout)]
2
3//! Build command - generates production Rust code from Dampen UI files
4
5use dampen_core::{generate_application, parse, HandlerSignature};
6use std::fs;
7use std::path::{Path, PathBuf};
8
9/// Build command arguments
10#[derive(clap::Args)]
11pub struct BuildArgs {
12    /// Input directory containing .dampen files (default: ui/)
13    #[arg(short, long, default_value = "ui")]
14    input: String,
15
16    /// Output file for generated code (default: src/ui_generated.rs)
17    #[arg(short, long, default_value = "src/ui_generated.rs")]
18    output: String,
19
20    /// Model struct name (default: Model)
21    #[arg(long, default_value = "Model")]
22    model: String,
23
24    /// Message enum name (default: Message)
25    #[arg(long, default_value = "Message")]
26    message: String,
27
28    /// Verbose output
29    #[arg(short, long)]
30    verbose: bool,
31
32    /// Production build (use cargo build --release)
33    #[arg(long)]
34    prod: bool,
35
36    /// Release build (use cargo build --release)
37    #[arg(long)]
38    release: bool,
39}
40
41/// Execute the build command
42pub fn execute(args: &BuildArgs) -> Result<(), String> {
43    if args.prod || args.release {
44        return execute_production_build(args);
45    }
46
47    if args.verbose {
48        eprintln!("Building from {} to {}", args.input, args.output);
49    }
50
51    // Find all .dampen files
52    let input_dir = PathBuf::from(&args.input);
53    if !input_dir.exists() {
54        return Err(format!("Input directory '{}' does not exist", args.input));
55    }
56
57    let dampen_files = find_dampen_files(&input_dir);
58    if dampen_files.is_empty() {
59        return Err(format!("No .dampen files found in '{}'", args.input));
60    }
61
62    if args.verbose {
63        eprintln!("Found {} .dampen files", dampen_files.len());
64    }
65
66    // Generate code from all files
67    let generated_code =
68        generate_code_from_files(&dampen_files, &args.model, &args.message, args.verbose)?;
69
70    // Write to output file
71    let output_path = PathBuf::from(&args.output);
72    if let Some(parent) = output_path.parent() {
73        fs::create_dir_all(parent)
74            .map_err(|e| format!("Failed to create output directory: {}", e))?;
75    }
76
77    fs::write(&output_path, generated_code)
78        .map_err(|e| format!("Failed to write output: {}", e))?;
79
80    if args.verbose {
81        eprintln!("Generated code written to {}", output_path.display());
82    }
83
84    eprintln!("Build successful!");
85    Ok(())
86}
87
88/// Find all .dampen files recursively
89fn find_dampen_files(dir: &Path) -> Vec<PathBuf> {
90    let mut files = Vec::new();
91
92    if let Ok(entries) = fs::read_dir(dir) {
93        for entry in entries.filter_map(|e| e.ok()) {
94            let path = entry.path();
95
96            if path.is_dir() {
97                files.extend(find_dampen_files(&path));
98            } else if path.extension().is_some_and(|ext| ext == "dampen") {
99                files.push(path);
100            }
101        }
102    }
103
104    files
105}
106
107/// Generate Rust code from multiple .dampen files
108fn generate_code_from_files(
109    files: &[PathBuf],
110    model_name: &str,
111    message_name: &str,
112    verbose: bool,
113) -> Result<String, String> {
114    let mut code = String::new();
115
116    // Add header
117    code.push_str("//! Auto-generated UI code\n");
118    code.push_str("//! DO NOT EDIT - Generated by `dampen build`\n\n");
119    code.push_str("use dampen_core::{parse, generate_application, HandlerSignature};\n\n");
120
121    // For now, we'll generate a single combined function
122    // In a full implementation, we'd parse handler signatures from a separate file
123    if files.len() > 1 {
124        eprintln!("Warning: Multiple .dampen files found. Using first file for generation.");
125    }
126
127    // Use the first file for now
128    let main_file = &files[0];
129    let content = fs::read_to_string(main_file)
130        .map_err(|e| format!("Failed to read {}: {}", main_file.display(), e))?;
131
132    if verbose {
133        eprintln!("Parsing {}", main_file.display());
134    }
135
136    // Parse the XML
137    let doc = parse(&content).map_err(|e| format!("Parse error: {}", e))?;
138
139    // For now, use empty handlers (in a full implementation, these would be discovered)
140    // In production, this would parse handler signatures from a separate file or derive them
141    let handlers: Vec<HandlerSignature> = vec![];
142
143    // Skip validation for now since we don't have handler signatures yet
144
145    // Generate code
146    let output = generate_application(&doc, model_name, message_name, &handlers)
147        .map_err(|e| format!("Code generation error: {}", e))?;
148
149    code.push_str(&output.code);
150
151    // Add warnings if any
152    if !output.warnings.is_empty() {
153        for warning in &output.warnings {
154            eprintln!("Warning: {}", warning);
155        }
156    }
157
158    Ok(code)
159}
160
161/// Execute production build using cargo build
162fn execute_production_build(args: &BuildArgs) -> Result<(), String> {
163    use std::process::Command;
164
165    if args.verbose {
166        eprintln!("Running production build...");
167        eprintln!("Mode: {}", if args.release { "release" } else { "debug" });
168    }
169
170    // Check if build.rs exists
171    if !Path::new("build.rs").exists() {
172        return Err(
173            "build.rs not found. This project may not be configured for production builds."
174                .to_string(),
175        );
176    }
177
178    // Check if Cargo.toml exists
179    if !Path::new("Cargo.toml").exists() {
180        return Err("Cargo.toml not found. Are you in a Rust project directory?".to_string());
181    }
182
183    // Build the cargo command
184    let mut cmd = Command::new("cargo");
185    cmd.arg("build");
186
187    if args.release {
188        cmd.arg("--release");
189    }
190
191    if args.verbose {
192        cmd.arg("--verbose");
193    }
194
195    // Execute cargo build
196    if args.verbose {
197        eprintln!(
198            "Executing: cargo build{}",
199            if args.release { " --release" } else { "" }
200        );
201    }
202
203    let status = cmd
204        .status()
205        .map_err(|e| format!("Failed to execute cargo: {}", e))?;
206
207    if !status.success() {
208        return Err("Build failed".to_string());
209    }
210
211    if args.verbose {
212        let binary_path = if args.release {
213            "target/release"
214        } else {
215            "target/debug"
216        };
217        eprintln!("Build successful! Binary is in {}/", binary_path);
218    }
219
220    eprintln!("Production build completed successfully!");
221    Ok(())
222}