use clap::{Parser, Subcommand};
use crate::cmd;
use crate::error::CliError;
#[derive(Parser, Debug)]
#[command(
name = "sz-rust",
bin_name = "sz-rust",
version,
about = "SZ-Rust 命令行工具 — 替代 PHP think 命令",
long_about = "SZ-Rust CLI 对齐 PHP ThinkPHP 6 think 命令体系,提供 make:migration / make:model / make:controller / migrate / route:list / cache:clear 等命令。"
)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Command>,
}
#[derive(Subcommand, Debug)]
pub enum Command {
#[command(name = "make")]
Make {
#[command(subcommand)]
make_command: cmd::make::MakeCommand,
},
#[command(name = "migrate")]
Migrate {
#[command(flatten)]
args: cmd::migrate::MigrateArgs,
},
#[command(name = "migrate:status")]
MigrateStatus {
#[arg(short = 'p', long, default_value = "migrations")]
path: String,
#[arg(long, default_value = "postgres")]
db_type: String,
#[arg(long)]
show_sql: bool,
#[arg(long)]
url: Option<String>,
},
#[command(name = "route:list")]
RouteList {
#[arg(short = 'f', long, default_value = "table")]
format: String,
},
#[command(name = "cache:clear")]
CacheClear {
#[arg(short = 's', long)]
store: Option<String>,
},
#[command(name = "db:seed")]
Seed {
#[arg(short = 'p', long, default_value = "seeds")]
path: String,
#[arg(long, default_value = "postgres")]
db_type: String,
#[arg(long)]
show_sql: bool,
#[arg(long)]
url: Option<String>,
#[arg(short = 'c', long)]
class: Option<String>,
},
#[command(name = "scheduler")]
Scheduler {
#[command(subcommand)]
scheduler_command: cmd::scheduler::SchedulerCommand,
},
#[command(name = "optimize:route")]
OptimizeRoute,
#[command(name = "optimize:config")]
OptimizeConfig,
#[command(name = "optimize:schema")]
OptimizeSchema,
#[command(name = "route:clear")]
RouteClear,
#[command(name = "plugin")]
Plugin {
#[command(subcommand)]
plugin_command: cmd::plugin::PluginCommand,
},
#[command(name = "admin")]
Admin {
#[command(subcommand)]
admin_command: cmd::admin::AdminCommand,
},
#[command(name = "serve")]
Serve {
#[arg(long)]
with_admin: bool,
#[arg(long)]
with_tenant: bool,
#[arg(long)]
with_data_scope: bool,
#[arg(long, default_value = "0.0.0.0:8080")]
addr: String,
#[arg(long)]
watch_config: bool,
#[arg(long)]
workers: Option<u16>,
#[arg(long)]
grace_timeout: Option<u16>,
#[arg(long)]
tls_cert: Option<std::path::PathBuf>,
#[arg(long)]
tls_key: Option<std::path::PathBuf>,
#[arg(long)]
access_log: bool,
#[arg(long, default_value_t = true)]
health: bool,
#[arg(long, conflicts_with = "health")]
no_health: bool,
},
#[command(name = "serve:reload")]
ServeReload,
#[command(name = "serve:log-level")]
ServeLogLevel,
}
impl Cli {
pub async fn execute(&self) -> Result<i32, CliError> {
match &self.command {
None => {
println!("SZ-Rust CLI — 使用 --help 查看可用命令");
Ok(0)
}
Some(Command::Make { make_command }) => {
cmd::make::execute(make_command).await.map(|_| 0)
}
Some(Command::Migrate { args }) => cmd::migrate::execute_migrate(args).await.map(|_| 0),
Some(Command::MigrateStatus {
path,
db_type,
show_sql,
url,
}) => cmd::migrate::execute_status_full(path, db_type, *show_sql, url.as_deref())
.await
.map(|_| 0),
Some(Command::RouteList { format }) => {
cmd::route::execute_route_list(format).map(|_| 0)
}
Some(Command::CacheClear { store }) => {
cmd::cache::execute_cache_clear(store.as_deref()).map(|_| 0)
}
Some(Command::Seed {
path,
db_type,
show_sql,
url,
class,
}) => {
let args = cmd::seed::SeedArgs {
path: path.clone(),
db_type: db_type.clone(),
show_sql: *show_sql,
url: url.clone(),
class: class.clone(),
};
cmd::seed::execute_seed(&args).map(|_| 0)
}
Some(Command::Scheduler { scheduler_command }) => {
cmd::scheduler::execute(scheduler_command).map(|_| 0)
}
Some(Command::OptimizeRoute) => {
cmd::optimize::execute_optimize_route().await.map(|_| 0)
}
Some(Command::OptimizeConfig) => {
cmd::optimize::execute_optimize_config().await.map(|_| 0)
}
Some(Command::OptimizeSchema) => {
cmd::optimize::execute_optimize_schema().await.map(|_| 0)
}
Some(Command::RouteClear) => cmd::optimize::execute_route_clear().await.map(|_| 0),
Some(Command::Plugin { plugin_command }) => cmd::plugin::execute(plugin_command).await,
Some(Command::Admin { admin_command }) => cmd::admin::execute(admin_command).await,
Some(Command::Serve {
with_admin,
with_tenant,
with_data_scope,
addr,
watch_config,
workers,
grace_timeout,
tls_cert,
tls_key,
access_log,
health,
no_health,
}) => {
let args = cmd::serve::ServeArgs {
with_admin: *with_admin,
with_tenant: *with_tenant,
with_data_scope: *with_data_scope,
addr: addr.clone(),
watch_config: *watch_config,
workers: *workers,
grace_timeout: *grace_timeout,
tls_cert: tls_cert.clone(),
tls_key: tls_key.clone(),
access_log: *access_log,
health: *health && !*no_health,
};
tokio::task::spawn_blocking(move || cmd::serve::execute(args))
.await
.map_err(|e| CliError::Generic(format!("serve 任务执行失败: {e}")))?
}
Some(Command::ServeReload) => {
println!("Windows 信号替代方案暂未实现,请使用 --watch-config 或重启服务");
Ok(0)
}
Some(Command::ServeLogLevel) => {
println!("Windows 信号替代方案暂未实现,请使用 --watch-config 或重启服务");
Ok(0)
}
}
}
}
#[cfg(test)]
#[allow(clippy::await_holding_lock)]
mod tests {
use super::*;
use clap::Parser;
#[test]
fn test_parse_make_model() {
let cli = Cli::parse_from(["sz-rust", "make", "model", "User"]);
match cli.command {
Some(Command::Make { make_command }) => {
assert!(matches!(make_command, cmd::make::MakeCommand::Model { .. }));
}
_ => panic!("expected Make command"),
}
}
#[test]
fn test_parse_make_controller() {
let cli = Cli::parse_from(["sz-rust", "make", "controller", "User"]);
match cli.command {
Some(Command::Make { make_command }) => {
assert!(matches!(
make_command,
cmd::make::MakeCommand::Controller { .. }
));
}
_ => panic!("expected Make command"),
}
}
#[test]
fn test_parse_make_migration() {
let cli = Cli::parse_from(["sz-rust", "make", "migration", "create_users"]);
match cli.command {
Some(Command::Make { make_command }) => {
assert!(matches!(
make_command,
cmd::make::MakeCommand::Migration { .. }
));
}
_ => panic!("expected Make command"),
}
}
#[test]
fn test_parse_optimize_schema() {
let cli = Cli::parse_from(["sz-rust", "optimize:schema"]);
assert!(matches!(cli.command, Some(Command::OptimizeSchema)));
}
#[test]
fn test_parse_make_validate() {
let cli = Cli::parse_from(["sz-rust", "make", "validate", "User"]);
match cli.command {
Some(Command::Make { make_command }) => {
assert!(matches!(
make_command,
cmd::make::MakeCommand::Validate { .. }
));
}
_ => panic!("expected Make command"),
}
}
#[test]
fn test_parse_make_seeder() {
let cli = Cli::parse_from(["sz-rust", "make", "seeder", "001_users"]);
match cli.command {
Some(Command::Make { make_command }) => {
assert!(matches!(
make_command,
cmd::make::MakeCommand::Seeder { .. }
));
}
_ => panic!("expected Make command"),
}
}
#[test]
fn test_parse_migrate() {
let cli = Cli::parse_from(["sz-rust", "migrate"]);
assert!(matches!(cli.command, Some(Command::Migrate { .. })));
}
#[test]
fn test_parse_migrate_status() {
let cli = Cli::parse_from(["sz-rust", "migrate:status"]);
assert!(matches!(cli.command, Some(Command::MigrateStatus { .. })));
}
#[test]
fn test_parse_route_list() {
let cli = Cli::parse_from(["sz-rust", "route:list"]);
assert!(matches!(cli.command, Some(Command::RouteList { .. })));
}
#[test]
fn test_parse_cache_clear() {
let cli = Cli::parse_from(["sz-rust", "cache:clear"]);
assert!(matches!(cli.command, Some(Command::CacheClear { .. })));
}
#[test]
fn test_parse_cache_clear_with_store() {
let cli = Cli::parse_from(["sz-rust", "cache:clear", "--store", "redis"]);
match cli.command {
Some(Command::CacheClear { store }) => {
assert_eq!(store.as_deref(), Some("redis"));
}
_ => panic!("expected CacheClear command"),
}
}
#[test]
fn test_parse_scheduler() {
let cli = Cli::parse_from(["sz-rust", "scheduler", "list"]);
assert!(matches!(cli.command, Some(Command::Scheduler { .. })));
}
#[test]
fn test_parse_db_seed() {
let cli = Cli::parse_from(["sz-rust", "db:seed"]);
assert!(matches!(cli.command, Some(Command::Seed { .. })));
}
#[test]
fn test_parse_db_seed_with_options() {
let cli = Cli::parse_from([
"sz-rust",
"db:seed",
"--path",
"custom_seeds",
"--db-type",
"mysql",
"--show-sql",
"--url",
"mysql://user:pass@host:3306/db",
"--class",
"001_users",
]);
match cli.command {
Some(Command::Seed {
path,
db_type,
show_sql,
url,
class,
}) => {
assert_eq!(path, "custom_seeds");
assert_eq!(db_type, "mysql");
assert!(show_sql);
assert_eq!(url.as_deref(), Some("mysql://user:pass@host:3306/db"));
assert_eq!(class.as_deref(), Some("001_users"));
}
_ => panic!("expected Seed command"),
}
}
#[tokio::test]
async fn test_execute_no_command_returns_ok() {
let cli = Cli { command: None };
let result = cli.execute().await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), 0);
}
#[tokio::test]
async fn test_execute_make_model() {
let _lock = crate::cmd::test_support::acquire_global_lock();
let temp = tempfile::tempdir().unwrap();
let original = std::env::current_dir().unwrap();
std::env::set_current_dir(temp.path()).unwrap();
let cli = Cli {
command: Some(Command::Make {
make_command: cmd::make::MakeCommand::Model {
name: "User".to_string(),
},
}),
};
let result = cli.execute().await;
std::env::set_current_dir(&original).unwrap();
assert!(result.is_ok());
assert!(temp.path().join("app/model/User.rs").exists());
}
#[tokio::test]
async fn test_execute_migrate_offline() {
let _lock = crate::cmd::test_support::acquire_global_lock();
let temp = tempfile::tempdir().unwrap();
let original = std::env::current_dir().unwrap();
std::env::set_current_dir(temp.path()).unwrap();
let cli = Cli {
command: Some(Command::Migrate {
args: cmd::migrate::MigrateArgs {
rollback: false,
path: temp.path().to_string_lossy().to_string(),
db_type: "postgres".to_string(),
show_sql: false,
url: None,
},
}),
};
let result = cli.execute().await;
std::env::set_current_dir(&original).unwrap();
assert!(result.is_ok());
}
#[tokio::test]
async fn test_execute_migrate_status_offline() {
let _lock = crate::cmd::test_support::acquire_global_lock();
let temp = tempfile::tempdir().unwrap();
let original = std::env::current_dir().unwrap();
std::env::set_current_dir(temp.path()).unwrap();
let cli = Cli {
command: Some(Command::MigrateStatus {
path: temp.path().to_string_lossy().to_string(),
db_type: "postgres".to_string(),
show_sql: false,
url: None,
}),
};
let result = cli.execute().await;
std::env::set_current_dir(&original).unwrap();
assert!(result.is_ok());
}
#[tokio::test]
async fn test_execute_route_list() {
let cli = Cli {
command: Some(Command::RouteList {
format: "table".to_string(),
}),
};
let result = cli.execute().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_execute_cache_clear() {
let _lock = crate::cmd::test_support::acquire_global_lock();
let temp = tempfile::tempdir().unwrap();
let original = std::env::current_dir().unwrap();
std::env::set_current_dir(temp.path()).unwrap();
let cli = Cli {
command: Some(Command::CacheClear { store: None }),
};
let result = cli.execute().await;
std::env::set_current_dir(&original).unwrap();
assert!(result.is_ok());
}
#[tokio::test]
async fn test_execute_seed_offline() {
let _lock = crate::cmd::test_support::acquire_global_lock();
let temp = tempfile::tempdir().unwrap();
let original = std::env::current_dir().unwrap();
std::env::set_current_dir(temp.path()).unwrap();
let cli = Cli {
command: Some(Command::Seed {
path: temp.path().to_string_lossy().to_string(),
db_type: "postgres".to_string(),
show_sql: false,
url: None,
class: None,
}),
};
let result = cli.execute().await;
std::env::set_current_dir(&original).unwrap();
assert!(result.is_ok());
}
#[tokio::test]
async fn test_execute_scheduler_list() {
let cli = Cli {
command: Some(Command::Scheduler {
scheduler_command: cmd::scheduler::SchedulerCommand::List {
config: std::path::PathBuf::from("/nonexistent/scheduler.toml"),
},
}),
};
let result = cli.execute().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_execute_optimize_route() {
let _lock = crate::cmd::test_support::acquire_global_lock();
let temp = tempfile::tempdir().unwrap();
let original = std::env::current_dir().unwrap();
std::env::set_current_dir(temp.path()).unwrap();
let cli = Cli {
command: Some(Command::OptimizeRoute),
};
let result = cli.execute().await;
std::env::set_current_dir(&original).unwrap();
assert!(result.is_ok());
}
#[tokio::test]
async fn test_execute_optimize_config() {
let _lock = crate::cmd::test_support::acquire_global_lock();
let temp = tempfile::tempdir().unwrap();
let original = std::env::current_dir().unwrap();
std::env::set_current_dir(temp.path()).unwrap();
std::fs::create_dir_all(temp.path().join("config")).unwrap();
let cli = Cli {
command: Some(Command::OptimizeConfig),
};
let result = cli.execute().await;
std::env::set_current_dir(&original).unwrap();
assert!(result.is_ok());
}
#[tokio::test]
async fn test_execute_optimize_schema() {
let _lock = crate::cmd::test_support::acquire_global_lock();
let temp = tempfile::tempdir().unwrap();
let original = std::env::current_dir().unwrap();
std::env::set_current_dir(temp.path()).unwrap();
let cli = Cli {
command: Some(Command::OptimizeSchema),
};
let result = cli.execute().await;
std::env::set_current_dir(&original).unwrap();
assert!(result.is_ok());
}
#[tokio::test]
async fn test_execute_route_clear() {
let _lock = crate::cmd::test_support::acquire_global_lock();
let temp = tempfile::tempdir().unwrap();
let original = std::env::current_dir().unwrap();
std::env::set_current_dir(temp.path()).unwrap();
let cli = Cli {
command: Some(Command::RouteClear),
};
let result = cli.execute().await;
std::env::set_current_dir(&original).unwrap();
assert!(result.is_ok());
}
#[tokio::test]
async fn test_execute_plugin_login_without_token() {
let cli = Cli {
command: Some(Command::Plugin {
plugin_command: cmd::plugin::PluginCommand::Login(cmd::plugin::LoginArgs {
token: None,
url: None,
}),
}),
};
let result = cli.execute().await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), 1);
}
#[tokio::test]
async fn test_execute_serve_reload() {
let cli = Cli {
command: Some(Command::ServeReload),
};
let result = cli.execute().await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), 0);
}
#[tokio::test]
async fn test_execute_serve_log_level() {
let cli = Cli {
command: Some(Command::ServeLogLevel),
};
let result = cli.execute().await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), 0);
}
#[tokio::test]
async fn test_execute_admin_via_parse_list_routes() {
let cli = Cli::parse_from(["sz-rust", "admin", "list-routes"]);
let result = cli.execute().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_execute_admin_via_parse_list_capabilities() {
let cli = Cli::parse_from(["sz-rust", "admin", "list-capabilities"]);
let result = cli.execute().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_execute_admin_via_parse_migrate_offline() {
let cli = Cli::parse_from(["sz-rust", "admin", "migrate", "--show-sql"]);
let result = cli.execute().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_execute_admin_via_parse_init_offline() {
let cli = Cli::parse_from(["sz-rust", "admin", "init"]);
let result = cli.execute().await;
assert!(result.is_ok());
}
}