use std::collections::HashMap;
use crate::CliError;
#[derive(Debug, Clone)]
pub struct CommandSignature {
pub name: String,
pub description: String,
pub usage: String,
}
pub trait Command: Send + Sync {
fn signature(&self) -> CommandSignature;
fn execute(&self, args: &[String]) -> Result<i32, CliError>;
}
pub struct Console {
commands: HashMap<String, Box<dyn Command>>,
}
impl Console {
pub fn new() -> Self {
Self {
commands: HashMap::new(),
}
}
pub fn register(&mut self, command: Box<dyn Command>) -> &mut Self {
let name = command.signature().name;
self.commands.insert(name, command);
self
}
pub async fn run(&self, args: Vec<String>) -> Result<i32, CliError> {
if args.len() >= 2 {
if let Some(command) = self.commands.get(&args[1]) {
let cmd_args: &[String] = &args[2..];
return command.execute(cmd_args);
}
}
crate::run(args).await
}
pub fn list(&self) -> Vec<CommandSignature> {
self.commands.values().map(|cmd| cmd.signature()).collect()
}
fn format_list(&self) -> String {
let mut out = String::from("Available commands:");
let mut signatures = self.list();
signatures.sort_by(|a, b| a.name.cmp(&b.name));
for sig in signatures {
out.push_str(&format!("\n {:<20} {}", sig.name, sig.description));
}
out
}
pub fn print_list(&self) {
println!("{}", self.format_list());
}
}
impl Default for Console {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[allow(clippy::await_holding_lock)]
mod tests {
use super::*;
struct HelloCommand;
impl Command for HelloCommand {
fn signature(&self) -> CommandSignature {
CommandSignature {
name: "hello".to_string(),
description: "Print hello world".to_string(),
usage: "sz-rust hello".to_string(),
}
}
fn execute(&self, _args: &[String]) -> Result<i32, CliError> {
println!("Hello, World!");
Ok(0)
}
}
struct EchoCommand;
impl Command for EchoCommand {
fn signature(&self) -> CommandSignature {
CommandSignature {
name: "echo".to_string(),
description: "Echo arguments".to_string(),
usage: "sz-rust echo <args...>".to_string(),
}
}
fn execute(&self, args: &[String]) -> Result<i32, CliError> {
println!("{}", args.join(" "));
Ok(0)
}
}
#[test]
fn test_register_and_list() {
let mut console = Console::new();
console.register(Box::new(HelloCommand));
let commands = console.list();
assert_eq!(commands.len(), 1);
assert_eq!(commands[0].name, "hello");
assert_eq!(commands[0].description, "Print hello world");
assert_eq!(commands[0].usage, "sz-rust hello");
}
#[test]
fn test_register_multiple_commands() {
let mut console = Console::new();
console
.register(Box::new(HelloCommand))
.register(Box::new(EchoCommand));
let commands = console.list();
assert_eq!(commands.len(), 2);
}
#[tokio::test]
async fn test_run_custom_command() {
let mut console = Console::new();
console.register(Box::new(HelloCommand));
let result = console
.run(vec!["sz-rust".to_string(), "hello".to_string()])
.await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), 0);
}
#[tokio::test]
async fn test_run_custom_command_with_args() {
let mut console = Console::new();
console.register(Box::new(EchoCommand));
let result = console
.run(vec![
"sz-rust".to_string(),
"echo".to_string(),
"foo".to_string(),
"bar".to_string(),
])
.await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), 0);
}
#[tokio::test]
async fn test_run_unknown_command_falls_through() {
let _lock = crate::cmd::test_support::acquire_global_lock();
let temp = tempfile::tempdir().unwrap();
let original = std::env::current_dir().ok();
std::env::set_current_dir(temp.path()).unwrap();
let console = Console::new();
let result = console
.run(vec!["sz-rust".to_string(), "cache:clear".to_string()])
.await;
if let Some(ref orig) = original {
let _ = std::env::set_current_dir(orig);
}
assert!(result.is_ok());
}
#[tokio::test]
async fn test_run_no_args_falls_through() {
let console = Console::new();
let result = console.run(vec!["sz-rust".to_string()]).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), 0);
}
#[test]
fn test_console_default_is_empty() {
let console = Console::default();
assert!(console.list().is_empty());
}
#[test]
fn test_register_overwrites_same_name() {
let mut console = Console::new();
console.register(Box::new(HelloCommand));
console.register(Box::new(EchoCommand));
let commands = console.list();
assert_eq!(commands.len(), 2);
}
#[test]
fn test_print_list_empty() {
let console = Console::new();
let commands = console.list();
assert!(commands.is_empty(), "空命令列表应返回空切片");
}
#[test]
fn test_print_list_with_commands() {
let mut console = Console::new();
console
.register(Box::new(HelloCommand))
.register(Box::new(EchoCommand));
let commands = console.list();
assert_eq!(commands.len(), 2, "应有两个注册命令");
}
#[test]
fn test_print_list_output_empty() {
let console = Console::new();
let out = console.format_list();
assert!(
out.starts_with("Available commands:"),
"空命令表也应输出表头"
);
assert_eq!(out.lines().count(), 1, "空命令表不应有命令行");
}
#[test]
fn test_print_list_output_with_commands() {
let mut console = Console::new();
console
.register(Box::new(HelloCommand))
.register(Box::new(EchoCommand));
let out = console.format_list();
assert!(out.contains("Available commands:"), "应包含表头");
assert!(out.contains("hello"), "应包含 hello 命令");
assert!(out.contains("echo"), "应包含 echo 命令");
let echo_pos = out.find("echo").expect("echo 应在列表中");
let hello_pos = out.find("hello").expect("hello 应在列表中");
assert!(
echo_pos < hello_pos,
"命令应按名称排序(echo 在 hello 之前)"
);
}
#[tokio::test]
async fn test_run_single_arg_falls_through() {
let console = Console::new();
let result = console.run(vec!["sz-rust".to_string()]).await;
assert!(result.is_ok());
}
}