Skip to main content

sz_rust_cli/
console.rs

1//! Console 模块 — 自定义命令注册与分发
2//!
3//! 对齐 PHP ThinkPHP 6 `think\console\Console` 与 `think\console\Command` 基类。
4//!
5//! ## PHP 对齐
6//!
7//! PHP ThinkPHP 通过 `think\console\Console` 注册并分发命令:
8//!
9//! ```php
10//! $console = new \think\Console();
11//! $console->add(new \app\command\Hello());
12//! $console->run();
13//! ```
14//!
15//! Rust 端通过 `Console` 结构体 + `Command` trait 实现等价功能:
16//!
17//! ```rust,ignore
18//! use sz_rust_cli::{Console, Command, CommandSignature};
19//!
20//! struct HelloCommand;
21//!
22//! impl Command for HelloCommand {
23//!     fn signature(&self) -> CommandSignature {
24//!         CommandSignature {
25//!             name: "hello".to_string(),
26//!             description: "Print hello world".to_string(),
27//!             usage: "sz-rust hello".to_string(),
28//!         }
29//!     }
30//!     fn execute(&self, _args: &[String]) -> Result<i32, sz_rust_cli::CliError> {
31//!         println!("Hello, World!");
32//!         Ok(0)
33//!     }
34//! }
35//!
36//! let mut console = Console::new();
37//! console.register(Box::new(HelloCommand));
38//! console.run(std::env::args().collect()).await;
39//! ```
40//!
41//! ## 设计说明
42//!
43//! - 内置命令(make / migrate / route / cache / scheduler)仍由 clap derive 处理
44//! - 自定义命令通过 `Command` trait 在运行时注册
45//! - `Console::run` 先检查首个参数是否匹配已注册的自定义命令,若不匹配则回退到 clap CLI
46
47use std::collections::HashMap;
48
49use crate::CliError;
50
51/// 控制台命令签名(名称、描述、用法)
52///
53/// 对齐 PHP `think\console\command\Command::configure()` 中设置的 name / description / help。
54#[derive(Debug, Clone)]
55pub struct CommandSignature {
56    /// 命令名称(如 `"make:model"`、`"hello"`)
57    pub name: String,
58    /// 简短描述
59    pub description: String,
60    /// 用法示例
61    pub usage: String,
62}
63
64/// 自定义控制台命令 trait
65///
66/// 对齐 PHP `think\console\Command` 抽象基类。
67///
68/// PHP 端通过继承 `Command` 并实现 `configure()` 与 `execute()` 方法定义命令;
69/// Rust 端通过实现此 trait 达到相同效果。
70pub trait Command: Send + Sync {
71    /// 获取命令签名
72    ///
73    /// 对齐 PHP `think\console\Command::configure()`。
74    fn signature(&self) -> CommandSignature;
75
76    /// 执行命令
77    ///
78    /// 对齐 PHP `think\console\Command::execute(Input $input, Output $output)`。
79    ///
80    /// # 参数
81    ///
82    /// - `args`:命令参数(不含程序名与命令名,仅含命令后的额外参数)
83    ///
84    /// # 返回
85    ///
86    /// - `Ok(0)`:成功
87    /// - `Ok(code)`:命令指定的退出码(非 0 表示部分失败)
88    /// - `Err(_)`:内部错误
89    fn execute(&self, args: &[String]) -> Result<i32, CliError>;
90}
91
92/// 控制台应用 — 命令注册表与分发器
93///
94/// 对齐 PHP `think\console\Console`。允许在运行时注册自定义命令,
95/// 并列出所有可用命令。
96///
97/// # 分发逻辑
98///
99/// 1. 若 `args[1]` 匹配已注册的自定义命令名,则分发到该命令
100/// 2. 否则,回退到内置的 clap CLI(`crate::run`)
101///
102/// # 示例
103///
104/// ```rust,ignore
105/// use sz_rust_cli::{Console, Command, CommandSignature};
106///
107/// let mut console = Console::new();
108/// console.register(Box::new(MyCommand));
109/// console.run(vec!["sz-rust".to_string(), "my:command".to_string()]).await;
110/// ```
111pub struct Console {
112    /// 已注册的自定义命令映射(命令名 → 命令实现)
113    commands: HashMap<String, Box<dyn Command>>,
114}
115
116impl Console {
117    /// 创建空的 Console 实例
118    pub fn new() -> Self {
119        Self {
120            commands: HashMap::new(),
121        }
122    }
123
124    /// 注册自定义命令
125    ///
126    /// 对齐 PHP `think\console\Console::add(Command $command)`。
127    ///
128    /// # 参数
129    ///
130    /// - `command`:实现了 `Command` trait 的命令实例(装箱)
131    ///
132    /// # 返回
133    ///
134    /// 返回 `&mut Self` 以支持链式调用。
135    ///
136    /// # 示例
137    ///
138    /// ```rust,ignore
139    /// let mut console = Console::new();
140    /// console
141    ///     .register(Box::new(HelloCommand))
142    ///     .register(Box::new(WorldCommand));
143    /// ```
144    pub fn register(&mut self, command: Box<dyn Command>) -> &mut Self {
145        let name = command.signature().name;
146        self.commands.insert(name, command);
147        self
148    }
149
150    /// 运行控制台
151    ///
152    /// 若 `args[1]`(程序名后的首个参数)匹配已注册的自定义命令名,
153    /// 则分发到该命令;否则回退到内置 clap CLI。
154    ///
155    /// # 参数
156    ///
157    /// - `args`:命令行参数(含程序名,如 `["sz-rust", "hello", "arg1"]`)
158    ///
159    /// # 返回
160    ///
161    /// - `Ok(0)`:成功
162    /// - `Ok(code)`:命令指定的退出码
163    /// - `Err(_)`:内部错误
164    pub async fn run(&self, args: Vec<String>) -> Result<i32, CliError> {
165        if args.len() >= 2 {
166            if let Some(command) = self.commands.get(&args[1]) {
167                let cmd_args: &[String] = &args[2..];
168                return command.execute(cmd_args);
169            }
170        }
171        crate::run(args).await
172    }
173
174    /// 列出所有已注册的自定义命令签名
175    ///
176    /// 对齐 PHP `think\console\Console::getCommands()`。
177    ///
178    /// # 返回
179    ///
180    /// 返回命令签名向量,顺序不保证(HashMap 迭代顺序不确定)。
181    pub fn list(&self) -> Vec<CommandSignature> {
182        self.commands.values().map(|cmd| cmd.signature()).collect()
183    }
184
185    /// 打印命令列表到标准输出
186    ///
187    /// 对齐 PHP `think\console\Console::listCommands()`。
188    pub fn print_list(&self) {
189        println!("Available commands:");
190        let mut signatures = self.list();
191        signatures.sort_by(|a, b| a.name.cmp(&b.name));
192        for sig in signatures {
193            println!("  {:<20} {}", sig.name, sig.description);
194        }
195    }
196}
197
198impl Default for Console {
199    fn default() -> Self {
200        Self::new()
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    /// 测试用命令:打印 Hello, World!
209    struct HelloCommand;
210
211    impl Command for HelloCommand {
212        fn signature(&self) -> CommandSignature {
213            CommandSignature {
214                name: "hello".to_string(),
215                description: "Print hello world".to_string(),
216                usage: "sz-rust hello".to_string(),
217            }
218        }
219
220        fn execute(&self, _args: &[String]) -> Result<i32, CliError> {
221            println!("Hello, World!");
222            Ok(0)
223        }
224    }
225
226    /// 测试用命令:回显参数
227    struct EchoCommand;
228
229    impl Command for EchoCommand {
230        fn signature(&self) -> CommandSignature {
231            CommandSignature {
232                name: "echo".to_string(),
233                description: "Echo arguments".to_string(),
234                usage: "sz-rust echo <args...>".to_string(),
235            }
236        }
237
238        fn execute(&self, args: &[String]) -> Result<i32, CliError> {
239            println!("{}", args.join(" "));
240            Ok(0)
241        }
242    }
243
244    #[test]
245    fn test_register_and_list() {
246        let mut console = Console::new();
247        console.register(Box::new(HelloCommand));
248        let commands = console.list();
249        assert_eq!(commands.len(), 1);
250        assert_eq!(commands[0].name, "hello");
251        assert_eq!(commands[0].description, "Print hello world");
252        assert_eq!(commands[0].usage, "sz-rust hello");
253    }
254
255    #[test]
256    fn test_register_multiple_commands() {
257        let mut console = Console::new();
258        console
259            .register(Box::new(HelloCommand))
260            .register(Box::new(EchoCommand));
261        let commands = console.list();
262        assert_eq!(commands.len(), 2);
263    }
264
265    #[tokio::test]
266    async fn test_run_custom_command() {
267        let mut console = Console::new();
268        console.register(Box::new(HelloCommand));
269        let result = console
270            .run(vec!["sz-rust".to_string(), "hello".to_string()])
271            .await;
272        assert!(result.is_ok());
273        assert_eq!(result.unwrap(), 0);
274    }
275
276    #[tokio::test]
277    async fn test_run_custom_command_with_args() {
278        let mut console = Console::new();
279        console.register(Box::new(EchoCommand));
280        let result = console
281            .run(vec![
282                "sz-rust".to_string(),
283                "echo".to_string(),
284                "foo".to_string(),
285                "bar".to_string(),
286            ])
287            .await;
288        assert!(result.is_ok());
289        assert_eq!(result.unwrap(), 0);
290    }
291
292    #[tokio::test]
293    async fn test_run_unknown_command_falls_through() {
294        let console = Console::new();
295        // "cache:clear" 是内置 clap 命令,未注册为自定义命令,应回退到 crate::run
296        let result = console
297            .run(vec!["sz-rust".to_string(), "cache:clear".to_string()])
298            .await;
299        assert!(result.is_ok());
300    }
301
302    #[tokio::test]
303    async fn test_run_no_args_falls_through() {
304        let console = Console::new();
305        // 无参数时应回退到 crate::run
306        let result = console.run(vec!["sz-rust".to_string()]).await;
307        assert!(result.is_ok());
308        assert_eq!(result.unwrap(), 0);
309    }
310
311    #[test]
312    fn test_console_default_is_empty() {
313        let console = Console::default();
314        assert!(console.list().is_empty());
315    }
316
317    #[test]
318    fn test_register_overwrites_same_name() {
319        let mut console = Console::new();
320        console.register(Box::new(HelloCommand));
321        // 注册同名命令应覆盖
322        console.register(Box::new(EchoCommand));
323        let commands = console.list();
324        // HelloCommand 和 EchoCommand 名称不同,故应为 2
325        assert_eq!(commands.len(), 2);
326    }
327}