1use crate::error::{CommandError, Result};
2use crate::template::TemplateProcessor;
3use crate::types::{CommandContext, CommandDefinition, CommandExecutionResult};
4use std::collections::HashMap;
5use std::process::Command;
6use std::time::Instant;
7
8pub struct CommandExecutor;
10
11impl CommandExecutor {
12 pub fn execute(
14 command_def: &CommandDefinition,
15 context: &CommandContext,
16 ) -> Result<CommandExecutionResult> {
17 if !command_def.enabled {
19 return Err(CommandError::ExecutionFailed(format!(
20 "Command is disabled: {}",
21 command_def.id
22 )));
23 }
24
25 let processed_command =
27 TemplateProcessor::process(&command_def.command, &context.arguments)?;
28
29 let start = Instant::now();
31
32 let output = if cfg!(target_os = "windows") {
34 Command::new("cmd")
35 .args(["/C", &processed_command])
36 .current_dir(&context.cwd)
37 .envs(&context.env)
38 .output()
39 .map_err(|e| CommandError::ExecutionFailed(e.to_string()))?
40 } else {
41 Command::new("sh")
42 .args(["-c", &processed_command])
43 .current_dir(&context.cwd)
44 .envs(&context.env)
45 .output()
46 .map_err(|e| CommandError::ExecutionFailed(e.to_string()))?
47 };
48
49 let duration = start.elapsed();
50
51 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
53 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
54 let exit_code = output.status.code().unwrap_or(-1);
55
56 if command_def.timeout_seconds > 0 && duration.as_secs() > command_def.timeout_seconds {
58 return Err(CommandError::ExecutionFailed(format!(
59 "Command execution timed out after {} seconds",
60 command_def.timeout_seconds
61 )));
62 }
63
64 Ok(CommandExecutionResult::new(&command_def.id, exit_code)
65 .with_stdout(stdout)
66 .with_stderr(stderr)
67 .with_duration(duration.as_millis() as u64))
68 }
69
70 pub fn execute_and_get_output(
72 command_def: &CommandDefinition,
73 context: &CommandContext,
74 ) -> Result<String> {
75 let result = Self::execute(command_def, context)?;
76
77 if result.success {
78 Ok(result.stdout)
79 } else {
80 Err(CommandError::ExecutionFailed(format!(
81 "Command failed with exit code {}: {}",
82 result.exit_code, result.stderr
83 )))
84 }
85 }
86
87 pub fn execute_and_get_all_output(
89 command_def: &CommandDefinition,
90 context: &CommandContext,
91 ) -> Result<(String, String)> {
92 let result = Self::execute(command_def, context)?;
93 Ok((result.stdout, result.stderr))
94 }
95
96 pub fn validate_arguments(
98 command_def: &CommandDefinition,
99 arguments: &HashMap<String, String>,
100 ) -> Result<()> {
101 for arg_def in &command_def.arguments {
102 if arg_def.required && !arguments.contains_key(&arg_def.name) {
103 return Err(CommandError::InvalidArgument(format!(
104 "Missing required argument: {}",
105 arg_def.name
106 )));
107 }
108
109 if let Some(value) = arguments.get(&arg_def.name) {
110 if let Some(pattern) = &arg_def.validation_pattern {
112 let regex = regex::Regex::new(pattern)?;
113 if !regex.is_match(value) {
114 return Err(CommandError::InvalidArgument(format!(
115 "Argument '{}' does not match pattern: {}",
116 arg_def.name, pattern
117 )));
118 }
119 }
120 }
121 }
122
123 Ok(())
124 }
125
126 pub fn build_context_with_defaults(
128 command_def: &CommandDefinition,
129 mut arguments: HashMap<String, String>,
130 cwd: String,
131 ) -> Result<CommandContext> {
132 for arg_def in &command_def.arguments {
134 if !arguments.contains_key(&arg_def.name) {
135 if let Some(default) = &arg_def.default {
136 arguments.insert(arg_def.name.clone(), default.clone());
137 }
138 }
139 }
140
141 Ok(CommandContext {
142 cwd,
143 env: std::env::vars().collect(),
144 arguments,
145 })
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::types::{ArgumentType, CommandArgument};
153
154 #[test]
155 fn test_validate_arguments_success() {
156 let mut cmd = CommandDefinition::new("test", "Test", "echo {{name}}");
157 cmd.arguments.push(
158 CommandArgument::new("name", ArgumentType::String)
159 .with_required(true)
160 .with_description("User name"),
161 );
162
163 let mut args = HashMap::new();
164 args.insert("name".to_string(), "Alice".to_string());
165
166 assert!(CommandExecutor::validate_arguments(&cmd, &args).is_ok());
167 }
168
169 #[test]
170 fn test_validate_arguments_missing_required() {
171 let mut cmd = CommandDefinition::new("test", "Test", "echo {{name}}");
172 cmd.arguments.push(
173 CommandArgument::new("name", ArgumentType::String)
174 .with_required(true)
175 .with_description("User name"),
176 );
177
178 let args = HashMap::new();
179 assert!(CommandExecutor::validate_arguments(&cmd, &args).is_err());
180 }
181
182 #[test]
183 fn test_validate_arguments_with_pattern() {
184 let mut cmd = CommandDefinition::new("test", "Test", "echo {{email}}");
185 cmd.arguments.push(
186 CommandArgument::new("email", ArgumentType::String)
187 .with_required(true)
188 .with_validation_pattern(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"),
189 );
190
191 let mut args = HashMap::new();
192 args.insert("email".to_string(), "invalid-email".to_string());
193 assert!(CommandExecutor::validate_arguments(&cmd, &args).is_err());
194
195 args.insert("email".to_string(), "test@example.com".to_string());
196 assert!(CommandExecutor::validate_arguments(&cmd, &args).is_ok());
197 }
198
199 #[test]
200 fn test_build_context_with_defaults() {
201 let mut cmd = CommandDefinition::new("test", "Test", "echo {{name}}");
202 cmd.arguments.push(
203 CommandArgument::new("name", ArgumentType::String)
204 .with_default("Guest")
205 .with_description("User name"),
206 );
207
208 let args = HashMap::new();
209 let context =
210 CommandExecutor::build_context_with_defaults(&cmd, args, ".".to_string()).unwrap();
211
212 assert_eq!(context.arguments.get("name").unwrap(), "Guest");
213 }
214
215 #[test]
216 fn test_build_context_override_defaults() {
217 let mut cmd = CommandDefinition::new("test", "Test", "echo {{name}}");
218 cmd.arguments.push(
219 CommandArgument::new("name", ArgumentType::String)
220 .with_default("Guest")
221 .with_description("User name"),
222 );
223
224 let mut args = HashMap::new();
225 args.insert("name".to_string(), "Alice".to_string());
226 let context =
227 CommandExecutor::build_context_with_defaults(&cmd, args, ".".to_string()).unwrap();
228
229 assert_eq!(context.arguments.get("name").unwrap(), "Alice");
230 }
231
232 #[test]
233 fn test_execute_simple_command() {
234 let cmd = CommandDefinition::new("test", "Test", "echo hello");
235 let context = CommandContext {
236 cwd: ".".to_string(),
237 env: std::env::vars().collect(),
238 arguments: HashMap::new(),
239 };
240
241 let result = CommandExecutor::execute(&cmd, &context).unwrap();
242 assert!(result.success);
243 assert!(result.stdout.contains("hello"));
244 }
245
246 #[test]
247 fn test_execute_disabled_command() {
248 let mut cmd = CommandDefinition::new("test", "Test", "echo hello");
249 cmd.enabled = false;
250
251 let context = CommandContext {
252 cwd: ".".to_string(),
253 env: std::env::vars().collect(),
254 arguments: HashMap::new(),
255 };
256
257 assert!(CommandExecutor::execute(&cmd, &context).is_err());
258 }
259
260 #[test]
261 fn test_execute_with_template() {
262 let cmd = CommandDefinition::new("test", "Test", "echo {{message}}");
263 let mut args = HashMap::new();
264 args.insert("message".to_string(), "Hello World".to_string());
265
266 let context = CommandContext {
267 cwd: ".".to_string(),
268 env: std::env::vars().collect(),
269 arguments: args,
270 };
271
272 let result = CommandExecutor::execute(&cmd, &context).unwrap();
273 assert!(result.success);
274 assert!(result.stdout.contains("Hello World"));
275 }
276}