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 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) => cmd::optimize::execute_optimize_route().map(|_| 0),
233            Some(Command::OptimizeConfig) => cmd::optimize::execute_optimize_config().map(|_| 0),
234            Some(Command::OptimizeSchema) => cmd::optimize::execute_optimize_schema().map(|_| 0),
235            Some(Command::RouteClear) => cmd::optimize::execute_route_clear().map(|_| 0),
236        }
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243    use clap::Parser;
244
245    #[test]
246    fn test_parse_make_model() {
247        let cli = Cli::parse_from(["sz-rust", "make", "model", "User"]);
248        match cli.command {
249            Some(Command::Make { make_command }) => {
250                assert!(matches!(make_command, cmd::make::MakeCommand::Model { .. }));
251            }
252            _ => panic!("expected Make command"),
253        }
254    }
255
256    #[test]
257    fn test_parse_make_controller() {
258        let cli = Cli::parse_from(["sz-rust", "make", "controller", "User"]);
259        match cli.command {
260            Some(Command::Make { make_command }) => {
261                assert!(matches!(
262                    make_command,
263                    cmd::make::MakeCommand::Controller { .. }
264                ));
265            }
266            _ => panic!("expected Make command"),
267        }
268    }
269
270    #[test]
271    fn test_parse_make_migration() {
272        let cli = Cli::parse_from(["sz-rust", "make", "migration", "create_users"]);
273        match cli.command {
274            Some(Command::Make { make_command }) => {
275                assert!(matches!(
276                    make_command,
277                    cmd::make::MakeCommand::Migration { .. }
278                ));
279            }
280            _ => panic!("expected Make command"),
281        }
282    }
283
284    #[test]
285    fn test_parse_optimize_schema() {
286        let cli = Cli::parse_from(["sz-rust", "optimize:schema"]);
287        assert!(matches!(cli.command, Some(Command::OptimizeSchema)));
288    }
289
290    #[test]
291    fn test_parse_make_validate() {
292        let cli = Cli::parse_from(["sz-rust", "make", "validate", "User"]);
293        match cli.command {
294            Some(Command::Make { make_command }) => {
295                assert!(matches!(
296                    make_command,
297                    cmd::make::MakeCommand::Validate { .. }
298                ));
299            }
300            _ => panic!("expected Make command"),
301        }
302    }
303
304    #[test]
305    fn test_parse_make_seeder() {
306        let cli = Cli::parse_from(["sz-rust", "make", "seeder", "001_users"]);
307        match cli.command {
308            Some(Command::Make { make_command }) => {
309                assert!(matches!(
310                    make_command,
311                    cmd::make::MakeCommand::Seeder { .. }
312                ));
313            }
314            _ => panic!("expected Make command"),
315        }
316    }
317
318    #[test]
319    fn test_parse_migrate() {
320        let cli = Cli::parse_from(["sz-rust", "migrate"]);
321        assert!(matches!(cli.command, Some(Command::Migrate { .. })));
322    }
323
324    #[test]
325    fn test_parse_migrate_status() {
326        let cli = Cli::parse_from(["sz-rust", "migrate:status"]);
327        assert!(matches!(cli.command, Some(Command::MigrateStatus { .. })));
328    }
329
330    #[test]
331    fn test_parse_route_list() {
332        let cli = Cli::parse_from(["sz-rust", "route:list"]);
333        assert!(matches!(cli.command, Some(Command::RouteList { .. })));
334    }
335
336    #[test]
337    fn test_parse_cache_clear() {
338        let cli = Cli::parse_from(["sz-rust", "cache:clear"]);
339        assert!(matches!(cli.command, Some(Command::CacheClear { .. })));
340    }
341
342    #[test]
343    fn test_parse_cache_clear_with_store() {
344        let cli = Cli::parse_from(["sz-rust", "cache:clear", "--store", "redis"]);
345        match cli.command {
346            Some(Command::CacheClear { store }) => {
347                assert_eq!(store.as_deref(), Some("redis"));
348            }
349            _ => panic!("expected CacheClear command"),
350        }
351    }
352
353    #[test]
354    fn test_parse_scheduler() {
355        let cli = Cli::parse_from(["sz-rust", "scheduler", "list"]);
356        assert!(matches!(cli.command, Some(Command::Scheduler { .. })));
357    }
358
359    #[test]
360    fn test_parse_db_seed() {
361        let cli = Cli::parse_from(["sz-rust", "db:seed"]);
362        assert!(matches!(cli.command, Some(Command::Seed { .. })));
363    }
364
365    #[test]
366    fn test_parse_db_seed_with_options() {
367        let cli = Cli::parse_from([
368            "sz-rust",
369            "db:seed",
370            "--path",
371            "custom_seeds",
372            "--db-type",
373            "mysql",
374            "--show-sql",
375            "--url",
376            "mysql://user:pass@host:3306/db",
377            "--class",
378            "001_users",
379        ]);
380        match cli.command {
381            Some(Command::Seed {
382                path,
383                db_type,
384                show_sql,
385                url,
386                class,
387            }) => {
388                assert_eq!(path, "custom_seeds");
389                assert_eq!(db_type, "mysql");
390                assert!(show_sql);
391                assert_eq!(url.as_deref(), Some("mysql://user:pass@host:3306/db"));
392                assert_eq!(class.as_deref(), Some("001_users"));
393            }
394            _ => panic!("expected Seed command"),
395        }
396    }
397
398    #[test]
399    fn test_execute_no_command_returns_ok() {
400        let cli = Cli { command: None };
401        let result = cli.execute();
402        assert!(result.is_ok());
403        assert_eq!(result.unwrap(), 0);
404    }
405}