1use std::collections::HashMap;
51
52use crate::CliError;
53
54#[derive(Debug, Clone)]
58pub struct CommandSignature {
59 pub name: String,
61 pub description: String,
63 pub usage: String,
65}
66
67pub trait Command: Send + Sync {
74 fn signature(&self) -> CommandSignature;
78
79 fn execute(&self, args: &[String]) -> Result<i32, CliError>;
93}
94
95pub struct Console {
115 commands: HashMap<String, Box<dyn Command>>,
117}
118
119impl Console {
120 pub fn new() -> Self {
122 Self {
123 commands: HashMap::new(),
124 }
125 }
126
127 pub fn register(&mut self, command: Box<dyn Command>) -> &mut Self {
148 let name = command.signature().name;
149 self.commands.insert(name, command);
150 self
151 }
152
153 pub async fn run(&self, args: Vec<String>) -> Result<i32, CliError> {
168 if args.len() >= 2 {
169 if let Some(command) = self.commands.get(&args[1]) {
170 let cmd_args: &[String] = &args[2..];
171 return command.execute(cmd_args);
172 }
173 }
174 crate::run(args).await
175 }
176
177 pub fn list(&self) -> Vec<CommandSignature> {
185 self.commands.values().map(|cmd| cmd.signature()).collect()
186 }
187
188 fn format_list(&self) -> String {
190 let mut out = String::from("Available commands:");
191 let mut signatures = self.list();
192 signatures.sort_by(|a, b| a.name.cmp(&b.name));
193 for sig in signatures {
194 out.push_str(&format!("\n {:<20} {}", sig.name, sig.description));
195 }
196 out
197 }
198
199 pub fn print_list(&self) {
203 println!("{}", self.format_list());
204 }
205}
206
207impl Default for Console {
208 fn default() -> Self {
209 Self::new()
210 }
211}
212
213#[cfg(test)]
214#[allow(clippy::await_holding_lock)]
215mod tests {
216 use super::*;
217
218 struct HelloCommand;
220
221 impl Command for HelloCommand {
222 fn signature(&self) -> CommandSignature {
223 CommandSignature {
224 name: "hello".to_string(),
225 description: "Print hello world".to_string(),
226 usage: "sz-rust hello".to_string(),
227 }
228 }
229
230 fn execute(&self, _args: &[String]) -> Result<i32, CliError> {
231 println!("Hello, World!");
232 Ok(0)
233 }
234 }
235
236 struct EchoCommand;
238
239 impl Command for EchoCommand {
240 fn signature(&self) -> CommandSignature {
241 CommandSignature {
242 name: "echo".to_string(),
243 description: "Echo arguments".to_string(),
244 usage: "sz-rust echo <args...>".to_string(),
245 }
246 }
247
248 fn execute(&self, args: &[String]) -> Result<i32, CliError> {
249 println!("{}", args.join(" "));
250 Ok(0)
251 }
252 }
253
254 #[test]
255 fn test_register_and_list() {
256 let mut console = Console::new();
257 console.register(Box::new(HelloCommand));
258 let commands = console.list();
259 assert_eq!(commands.len(), 1);
260 assert_eq!(commands[0].name, "hello");
261 assert_eq!(commands[0].description, "Print hello world");
262 assert_eq!(commands[0].usage, "sz-rust hello");
263 }
264
265 #[test]
266 fn test_register_multiple_commands() {
267 let mut console = Console::new();
268 console
269 .register(Box::new(HelloCommand))
270 .register(Box::new(EchoCommand));
271 let commands = console.list();
272 assert_eq!(commands.len(), 2);
273 }
274
275 #[tokio::test]
276 async fn test_run_custom_command() {
277 let mut console = Console::new();
278 console.register(Box::new(HelloCommand));
279 let result = console
280 .run(vec!["sz-rust".to_string(), "hello".to_string()])
281 .await;
282 assert!(result.is_ok());
283 assert_eq!(result.unwrap(), 0);
284 }
285
286 #[tokio::test]
287 async fn test_run_custom_command_with_args() {
288 let mut console = Console::new();
289 console.register(Box::new(EchoCommand));
290 let result = console
291 .run(vec![
292 "sz-rust".to_string(),
293 "echo".to_string(),
294 "foo".to_string(),
295 "bar".to_string(),
296 ])
297 .await;
298 assert!(result.is_ok());
299 assert_eq!(result.unwrap(), 0);
300 }
301
302 #[tokio::test]
303 async fn test_run_unknown_command_falls_through() {
304 let _lock = crate::cmd::test_support::acquire_global_lock();
305 let temp = tempfile::tempdir().unwrap();
306 let original = std::env::current_dir().ok();
307 std::env::set_current_dir(temp.path()).unwrap();
308 let console = Console::new();
309 let result = console
311 .run(vec!["sz-rust".to_string(), "cache:clear".to_string()])
312 .await;
313 if let Some(ref orig) = original {
314 let _ = std::env::set_current_dir(orig);
315 }
316 assert!(result.is_ok());
317 }
318
319 #[tokio::test]
320 async fn test_run_no_args_falls_through() {
321 let console = Console::new();
322 let result = console.run(vec!["sz-rust".to_string()]).await;
324 assert!(result.is_ok());
325 assert_eq!(result.unwrap(), 0);
326 }
327
328 #[test]
329 fn test_console_default_is_empty() {
330 let console = Console::default();
331 assert!(console.list().is_empty());
332 }
333
334 #[test]
335 fn test_register_overwrites_same_name() {
336 let mut console = Console::new();
337 console.register(Box::new(HelloCommand));
338 console.register(Box::new(EchoCommand));
340 let commands = console.list();
341 assert_eq!(commands.len(), 2);
343 }
344
345 #[test]
346 fn test_print_list_empty() {
347 let console = Console::new();
348 let commands = console.list();
349 assert!(commands.is_empty(), "空命令列表应返回空切片");
350 }
351
352 #[test]
353 fn test_print_list_with_commands() {
354 let mut console = Console::new();
355 console
356 .register(Box::new(HelloCommand))
357 .register(Box::new(EchoCommand));
358 let commands = console.list();
359 assert_eq!(commands.len(), 2, "应有两个注册命令");
360 }
361
362 #[test]
363 fn test_print_list_output_empty() {
364 let console = Console::new();
365 let out = console.format_list();
366 assert!(
367 out.starts_with("Available commands:"),
368 "空命令表也应输出表头"
369 );
370 assert_eq!(out.lines().count(), 1, "空命令表不应有命令行");
371 }
372
373 #[test]
374 fn test_print_list_output_with_commands() {
375 let mut console = Console::new();
376 console
377 .register(Box::new(HelloCommand))
378 .register(Box::new(EchoCommand));
379 let out = console.format_list();
380 assert!(out.contains("Available commands:"), "应包含表头");
381 assert!(out.contains("hello"), "应包含 hello 命令");
382 assert!(out.contains("echo"), "应包含 echo 命令");
383 let echo_pos = out.find("echo").expect("echo 应在列表中");
384 let hello_pos = out.find("hello").expect("hello 应在列表中");
385 assert!(
386 echo_pos < hello_pos,
387 "命令应按名称排序(echo 在 hello 之前)"
388 );
389 }
390
391 #[tokio::test]
392 async fn test_run_single_arg_falls_through() {
393 let console = Console::new();
394 let result = console.run(vec!["sz-rust".to_string()]).await;
395 assert!(result.is_ok());
396 }
397}