Skip to main content

sz_rust_cli/
cli.rs

1//! CLI 命令定义 — 基于 clap derive
2//!
3//! 对齐 PHP ThinkPHP 6 `think` 命令体系,借鉴 Laravel Artisan 风格。
4//!
5//! ## PHP 对齐
6//!
7//! PHP `think` 命令使用 Symfony Console 组件,通过 `configure()` 定义参数和选项。
8//! Rust 端使用 clap derive 宏,通过结构体字段定义参数和选项。
9//!
10//! ## 命令对照表
11//!
12//! | PHP | Rust | 说明 |
13//! |-----|------|------|
14//! | `php think make:model User` | `sz-rust make:model User` | 生成 Model |
15//! | `php think make:controller User` | `sz-rust make:controller User` | 生成 Controller |
16//! | `php think make:migration CreateUsers` | `sz-rust make:migration create_users` | 生成迁移文件 |
17//! | `php think make:seeder UserSeeder` | `sz-rust make:seeder user_seeder` | 生成填充文件 |
18//! | `php think make:validate User` | `sz-rust make:validate User` | 生成验证器 |
19//! | `php think make:event User` | `sz-rust make:event User` | 生成事件 |
20//! | `php think make:listener UserListener` | `sz-rust make:listener UserListener` | 生成监听器 |
21//! | `php think make:command Hello` | `sz-rust make:command Hello` | 生成命令 |
22//! | `php think make:service UserService` | `sz-rust make:service UserService` | 生成服务 |
23//! | `php think migrate` | `sz-rust migrate` | 执行迁移 |
24//! | `php think migrate:rollback` | `sz-rust migrate --rollback` | 回滚迁移 |
25//! | `php think migrate:status` | `sz-rust migrate:status` | 迁移状态 |
26//! | `php think db:seed` | `sz-rust db:seed` | 数据填充 |
27//! | `php think route:list` | `sz-rust route:list` | 路由列表 |
28//! | `php think cache:clear` | `sz-rust cache:clear` | 清空缓存 |
29//! | `php think optimize:route` | `sz-rust optimize:route` | 路由缓存 |
30//! | `php think optimize:config` | `sz-rust optimize:config` | 配置缓存 |
31//! | `php think optimize:schema` | `sz-rust optimize:schema` | 数据表字段缓存 |
32//! | `php think route:clear` | `sz-rust route:clear` | 清除路由缓存 |
33
34use clap::{Parser, Subcommand};
35
36use crate::cmd;
37use crate::error::CliError;
38
39/// SZ-Rust 命令行工具
40///
41/// 替代 PHP `think` 命令,提供代码生成、数据库迁移、路由查看、缓存管理等功能。
42#[derive(Parser, Debug)]
43#[command(
44    name = "sz-rust",
45    bin_name = "sz-rust",
46    version,
47    about = "SZ-Rust 命令行工具 — 替代 PHP think 命令",
48    long_about = "SZ-Rust CLI 对齐 PHP ThinkPHP 6 think 命令体系,提供 make:migration / make:model / make:controller / migrate / route:list / cache:clear 等命令。"
49)]
50pub struct Cli {
51    /// 子命令
52    #[command(subcommand)]
53    pub command: Option<Command>,
54}
55
56/// 顶层命令枚举
57///
58/// 对齐 PHP `think` 的命令分组(make / migrate / route / cache)。
59#[derive(Subcommand, Debug)]
60pub enum Command {
61    /// 代码生成命令组(make:migration / make:model / make:controller / make:guard / make:scaffold)
62    #[command(name = "make")]
63    Make {
64        /// make 子命令
65        #[command(subcommand)]
66        make_command: cmd::make::MakeCommand,
67    },
68
69    /// 数据库迁移命令(migrate / migrate:status / migrate:rollback)
70    #[command(name = "migrate")]
71    Migrate {
72        /// 迁移子命令参数
73        #[command(flatten)]
74        args: cmd::migrate::MigrateArgs,
75    },
76
77    /// 迁移状态查询(对齐 PHP `php think migrate:status`)
78    #[command(name = "migrate:status")]
79    MigrateStatus {
80        /// 迁移目录(默认 `migrations`)
81        #[arg(short = 'p', long, default_value = "migrations")]
82        path: String,
83
84        /// 数据库类型(默认 `postgres`,对齐 sz-orm `DbType`)
85        #[arg(long, default_value = "postgres")]
86        db_type: String,
87
88        /// 打印每个迁移的 SQL 内容
89        #[arg(long)]
90        show_sql: bool,
91
92        /// 数据库连接 URL(启用在线模式,查询真实状态)
93        ///
94        /// 省略时为离线模式,所有迁移状态显示为 `Pending*`。
95        #[arg(long)]
96        url: Option<String>,
97    },
98
99    /// 路由列表(对齐 PHP `php think route:list`)
100    #[command(name = "route:list")]
101    RouteList {
102        /// 输出格式(table / json)
103        #[arg(short = 'f', long, default_value = "table")]
104        format: String,
105    },
106
107    /// 清空缓存(对齐 PHP `php think cache:clear`)
108    #[command(name = "cache:clear")]
109    CacheClear {
110        /// 指定缓存存储名(默认清空所有)
111        #[arg(short = 's', long)]
112        store: Option<String>,
113    },
114
115    /// 数据填充(对齐 PHP `php think db:seed`)
116    ///
117    /// 从 `seeds/` 目录加载 `.sql` 文件并执行。提供 `--url` 时连接数据库真实执行,
118    /// 否则为离线模式(仅打印待执行内容)。
119    #[command(name = "db:seed")]
120    Seed {
121        /// 填充目录(默认 `seeds`)
122        #[arg(short = 'p', long, default_value = "seeds")]
123        path: String,
124
125        /// 数据库类型(默认 `postgres`,对齐 sz-orm `DbType`)
126        #[arg(long, default_value = "postgres")]
127        db_type: String,
128
129        /// 打印每个填充文件的 SQL 内容
130        #[arg(long)]
131        show_sql: bool,
132
133        /// 数据库连接 URL(启用在线模式)
134        ///
135        /// 省略时为离线模式,仅打印待执行的 SQL。
136        #[arg(long)]
137        url: Option<String>,
138
139        /// 指定填充器文件名(不含扩展名,如 `001_users_seed`)
140        ///
141        /// 省略时执行目录下所有 `.sql` 文件。
142        #[arg(short = 'c', long)]
143        class: Option<String>,
144    },
145
146    /// 调度器命令组(scheduler:list / scheduler:run / scheduler:start)
147    #[command(name = "scheduler")]
148    Scheduler {
149        /// scheduler 子命令
150        #[command(subcommand)]
151        scheduler_command: cmd::scheduler::SchedulerCommand,
152    },
153
154    /// 生成路由缓存(对齐 PHP `php think optimize:route`)
155    ///
156    /// 收集路由元数据,序列化为 JSON 写入 `runtime/cache/route_cache.json`。
157    #[command(name = "optimize:route")]
158    OptimizeRoute,
159
160    /// 生成配置缓存(对齐 PHP `php think optimize:config`)
161    ///
162    /// 扫描 `config/` 目录,合并所有配置,序列化为 JSON 写入 `runtime/cache/config_cache.json`。
163    #[command(name = "optimize:config")]
164    OptimizeConfig,
165
166    /// 生成数据表字段缓存(对齐 PHP `php think optimize:schema`)
167    ///
168    /// 读取 `config/database.yml` 数据库连接配置,生成 schema 缓存索引文件
169    /// (`runtime/schema_cache.json` + `runtime/schema_cache.php`)。
170    /// 业务方运行时通过 `SchemaCache::remember_schema()` 填充具体字段信息。
171    #[command(name = "optimize:schema")]
172    OptimizeSchema,
173
174    /// 清除路由缓存(对齐 PHP `php think route:clear`)
175    ///
176    /// 删除 `runtime/cache/route_cache.json` 文件。
177    #[command(name = "route:clear")]
178    RouteClear,
179}
180
181impl Cli {
182    /// 执行命令
183    ///
184    /// 根据 `command` 字段分发到对应的命令处理器。
185    ///
186    /// # 返回
187    ///
188    /// - `Ok(0)`:成功
189    /// - `Ok(code)`:命令指定的退出码(非 0 表示部分失败)
190    /// - `Err(_)`:内部错误
191    pub async fn execute(&self) -> Result<i32, CliError> {
192        match &self.command {
193            None => {
194                // 无子命令,打印帮助
195                println!("SZ-Rust CLI — 使用 --help 查看可用命令");
196                Ok(0)
197            }
198            Some(Command::Make { make_command }) => cmd::make::execute(make_command).map(|_| 0),
199            Some(Command::Migrate { args }) => cmd::migrate::execute_migrate(args).map(|_| 0),
200            Some(Command::MigrateStatus {
201                path,
202                db_type,
203                show_sql,
204                url,
205            }) => cmd::migrate::execute_status_full(path, db_type, *show_sql, url.as_deref())
206                .map(|_| 0),
207            Some(Command::RouteList { format }) => {
208                cmd::route::execute_route_list(format).map(|_| 0)
209            }
210            Some(Command::CacheClear { store }) => {
211                cmd::cache::execute_cache_clear(store.as_deref()).map(|_| 0)
212            }
213            Some(Command::Seed {
214                path,
215                db_type,
216                show_sql,
217                url,
218                class,
219            }) => {
220                let args = cmd::seed::SeedArgs {
221                    path: path.clone(),
222                    db_type: db_type.clone(),
223                    show_sql: *show_sql,
224                    url: url.clone(),
225                    class: class.clone(),
226                };
227                cmd::seed::execute_seed(&args).map(|_| 0)
228            }
229            Some(Command::Scheduler { scheduler_command }) => {
230                cmd::scheduler::execute(scheduler_command).map(|_| 0)
231            }
232            Some(Command::OptimizeRoute) => {
233                cmd::optimize::execute_optimize_route().await.map(|_| 0)
234            }
235            Some(Command::OptimizeConfig) => {
236                cmd::optimize::execute_optimize_config().await.map(|_| 0)
237            }
238            Some(Command::OptimizeSchema) => {
239                cmd::optimize::execute_optimize_schema().await.map(|_| 0)
240            }
241            Some(Command::RouteClear) => cmd::optimize::execute_route_clear().await.map(|_| 0),
242        }
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249    use clap::Parser;
250
251    #[test]
252    fn test_parse_make_model() {
253        let cli = Cli::parse_from(["sz-rust", "make", "model", "User"]);
254        match cli.command {
255            Some(Command::Make { make_command }) => {
256                assert!(matches!(make_command, cmd::make::MakeCommand::Model { .. }));
257            }
258            _ => panic!("expected Make command"),
259        }
260    }
261
262    #[test]
263    fn test_parse_make_controller() {
264        let cli = Cli::parse_from(["sz-rust", "make", "controller", "User"]);
265        match cli.command {
266            Some(Command::Make { make_command }) => {
267                assert!(matches!(
268                    make_command,
269                    cmd::make::MakeCommand::Controller { .. }
270                ));
271            }
272            _ => panic!("expected Make command"),
273        }
274    }
275
276    #[test]
277    fn test_parse_make_migration() {
278        let cli = Cli::parse_from(["sz-rust", "make", "migration", "create_users"]);
279        match cli.command {
280            Some(Command::Make { make_command }) => {
281                assert!(matches!(
282                    make_command,
283                    cmd::make::MakeCommand::Migration { .. }
284                ));
285            }
286            _ => panic!("expected Make command"),
287        }
288    }
289
290    #[test]
291    fn test_parse_optimize_schema() {
292        let cli = Cli::parse_from(["sz-rust", "optimize:schema"]);
293        assert!(matches!(cli.command, Some(Command::OptimizeSchema)));
294    }
295
296    #[test]
297    fn test_parse_make_validate() {
298        let cli = Cli::parse_from(["sz-rust", "make", "validate", "User"]);
299        match cli.command {
300            Some(Command::Make { make_command }) => {
301                assert!(matches!(
302                    make_command,
303                    cmd::make::MakeCommand::Validate { .. }
304                ));
305            }
306            _ => panic!("expected Make command"),
307        }
308    }
309
310    #[test]
311    fn test_parse_make_seeder() {
312        let cli = Cli::parse_from(["sz-rust", "make", "seeder", "001_users"]);
313        match cli.command {
314            Some(Command::Make { make_command }) => {
315                assert!(matches!(
316                    make_command,
317                    cmd::make::MakeCommand::Seeder { .. }
318                ));
319            }
320            _ => panic!("expected Make command"),
321        }
322    }
323
324    #[test]
325    fn test_parse_migrate() {
326        let cli = Cli::parse_from(["sz-rust", "migrate"]);
327        assert!(matches!(cli.command, Some(Command::Migrate { .. })));
328    }
329
330    #[test]
331    fn test_parse_migrate_status() {
332        let cli = Cli::parse_from(["sz-rust", "migrate:status"]);
333        assert!(matches!(cli.command, Some(Command::MigrateStatus { .. })));
334    }
335
336    #[test]
337    fn test_parse_route_list() {
338        let cli = Cli::parse_from(["sz-rust", "route:list"]);
339        assert!(matches!(cli.command, Some(Command::RouteList { .. })));
340    }
341
342    #[test]
343    fn test_parse_cache_clear() {
344        let cli = Cli::parse_from(["sz-rust", "cache:clear"]);
345        assert!(matches!(cli.command, Some(Command::CacheClear { .. })));
346    }
347
348    #[test]
349    fn test_parse_cache_clear_with_store() {
350        let cli = Cli::parse_from(["sz-rust", "cache:clear", "--store", "redis"]);
351        match cli.command {
352            Some(Command::CacheClear { store }) => {
353                assert_eq!(store.as_deref(), Some("redis"));
354            }
355            _ => panic!("expected CacheClear command"),
356        }
357    }
358
359    #[test]
360    fn test_parse_scheduler() {
361        let cli = Cli::parse_from(["sz-rust", "scheduler", "list"]);
362        assert!(matches!(cli.command, Some(Command::Scheduler { .. })));
363    }
364
365    #[test]
366    fn test_parse_db_seed() {
367        let cli = Cli::parse_from(["sz-rust", "db:seed"]);
368        assert!(matches!(cli.command, Some(Command::Seed { .. })));
369    }
370
371    #[test]
372    fn test_parse_db_seed_with_options() {
373        let cli = Cli::parse_from([
374            "sz-rust",
375            "db:seed",
376            "--path",
377            "custom_seeds",
378            "--db-type",
379            "mysql",
380            "--show-sql",
381            "--url",
382            "mysql://user:pass@host:3306/db",
383            "--class",
384            "001_users",
385        ]);
386        match cli.command {
387            Some(Command::Seed {
388                path,
389                db_type,
390                show_sql,
391                url,
392                class,
393            }) => {
394                assert_eq!(path, "custom_seeds");
395                assert_eq!(db_type, "mysql");
396                assert!(show_sql);
397                assert_eq!(url.as_deref(), Some("mysql://user:pass@host:3306/db"));
398                assert_eq!(class.as_deref(), Some("001_users"));
399            }
400            _ => panic!("expected Seed command"),
401        }
402    }
403
404    #[tokio::test]
405    async fn test_execute_no_command_returns_ok() {
406        let cli = Cli { command: None };
407        let result = cli.execute().await;
408        assert!(result.is_ok());
409        assert_eq!(result.unwrap(), 0);
410    }
411}