diff --git a/sea-orm-cli/Cargo.toml b/sea-orm-cli/Cargo.toml index 7f5c41c69..c612fa843 100644 --- a/sea-orm-cli/Cargo.toml +++ b/sea-orm-cli/Cargo.toml @@ -29,7 +29,7 @@ path = "src/bin/sea.rs" required-features = ["codegen"] [dependencies] -clap = { version = "^2.33.3" } +clap = { version = "^3.2", features = ["env", "derive"] } dotenv = { version = "^0.15" } async-std = { version = "^1.9", features = [ "attributes", "tokio1" ] } sea-orm-codegen = { version = "^0.8.0", path = "../sea-orm-codegen", optional = true } diff --git a/sea-orm-cli/src/bin/main.rs b/sea-orm-cli/src/bin/main.rs index 6861ffd76..ce41c6162 100644 --- a/sea-orm-cli/src/bin/main.rs +++ b/sea-orm-cli/src/bin/main.rs @@ -1,17 +1,24 @@ +use clap::StructOpt; use dotenv::dotenv; -use sea_orm_cli::*; +use sea_orm_cli::{handle_error, run_generate_command, run_migrate_command, Cli, Commands}; #[async_std::main] async fn main() { dotenv().ok(); - let matches = cli::build_cli().get_matches(); + let cli = Cli::parse(); + let verbose = cli.verbose; - match matches.subcommand() { - ("generate", Some(matches)) => run_generate_command(matches) - .await + match cli.command { + Commands::Generate { command } => { + run_generate_command(command, verbose) + .await + .unwrap_or_else(handle_error); + } + Commands::Migrate { + migration_dir, + command, + } => run_migrate_command(command, migration_dir.as_str(), verbose) .unwrap_or_else(handle_error), - ("migrate", Some(matches)) => run_migrate_command(matches).unwrap_or_else(handle_error), - _ => unreachable!("You should never see this message"), } } diff --git a/sea-orm-cli/src/bin/sea.rs b/sea-orm-cli/src/bin/sea.rs index fac8eb2b7..cc945f95f 100644 --- a/sea-orm-cli/src/bin/sea.rs +++ b/sea-orm-cli/src/bin/sea.rs @@ -1,19 +1,26 @@ //! COPY FROM bin/main.rs +use clap::StructOpt; use dotenv::dotenv; -use sea_orm_cli::*; +use sea_orm_cli::{handle_error, run_generate_command, run_migrate_command, Cli, Commands}; #[async_std::main] async fn main() { dotenv().ok(); - let matches = cli::build_cli().get_matches(); + let cli = Cli::parse(); + let verbose = cli.verbose; - match matches.subcommand() { - ("generate", Some(matches)) => run_generate_command(matches) - .await + match cli.command { + Commands::Generate { command } => { + run_generate_command(command, verbose) + .await + .unwrap_or_else(handle_error); + } + Commands::Migrate { + migration_dir, + command, + } => run_migrate_command(command, migration_dir.as_str(), verbose) .unwrap_or_else(handle_error), - ("migrate", Some(matches)) => run_migrate_command(matches).unwrap_or_else(handle_error), - _ => unreachable!("You should never see this message"), } } diff --git a/sea-orm-cli/src/cli.rs b/sea-orm-cli/src/cli.rs index 1a0ebfe68..39a062ed9 100644 --- a/sea-orm-cli/src/cli.rs +++ b/sea-orm-cli/src/cli.rs @@ -1,129 +1,160 @@ -use crate::migration::get_subcommands; -use clap::{App, AppSettings, Arg, SubCommand}; +use clap::{ArgGroup, Parser, Subcommand}; -pub fn build_cli() -> App<'static, 'static> { - let entity_subcommand = SubCommand::with_name("generate") - .about("Codegen related commands") - .setting(AppSettings::VersionlessSubcommands) - .subcommand( - SubCommand::with_name("entity") - .about("Generate entity") - .arg( - Arg::with_name("DATABASE_URL") - .long("database-url") - .short("u") - .help("Database URL") - .takes_value(true) - .required(true) - .env("DATABASE_URL"), - ) - .arg( - Arg::with_name("DATABASE_SCHEMA") - .long("database-schema") - .short("s") - .help("Database schema") - .long_help("Database schema\n \ +#[derive(Parser, Debug)] +#[clap(version)] +pub struct Cli { + #[clap(action, global = true, short, long, help = "Show debug messages")] + pub verbose: bool, + + #[clap(subcommand)] + pub command: Commands, +} + +#[derive(Subcommand, PartialEq, Debug)] +pub enum Commands { + #[clap(about = "Codegen related commands")] + #[clap(arg_required_else_help = true)] + Generate { + #[clap(subcommand)] + command: GenerateSubcommands, + }, + #[clap(about = "Migration related commands")] + Migrate { + #[clap( + value_parser, + global = true, + short = 'd', + long, + help = "Migration script directory", + default_value = "./migration" + )] + migration_dir: String, + + #[clap(subcommand)] + command: Option, + }, +} + +#[derive(Subcommand, PartialEq, Debug)] +pub enum MigrateSubcommands { + #[clap(about = "Initialize migration directory")] + Init, + #[clap(about = "Generate a new, empty migration")] + Generate { + #[clap( + value_parser, + long, + required = true, + help = "Name of the new migration" + )] + migration_name: String, + }, + #[clap(about = "Drop all tables from the database, then reapply all migrations")] + Fresh, + #[clap(about = "Rollback all applied migrations, then reapply all migrations")] + Refresh, + #[clap(about = "Rollback all applied migrations")] + Reset, + #[clap(about = "Check the status of all migrations")] + Status, + #[clap(about = "Apply pending migrations")] + Up { + #[clap( + value_parser, + short, + long, + default_value = "1", + help = "Number of pending migrations to be rolled back" + )] + num: u32, + }, + #[clap(value_parser, about = "Rollback applied migrations")] + Down { + #[clap( + value_parser, + short, + long, + default_value = "1", + help = "Number of pending migrations to be rolled back" + )] + num: u32, + }, +} + +#[derive(Subcommand, PartialEq, Debug)] +pub enum GenerateSubcommands { + #[clap(about = "Generate entity")] + #[clap(arg_required_else_help = true)] + #[clap(group(ArgGroup::new("formats").args(&["compact-format", "expanded-format"])))] + #[clap(group(ArgGroup::new("group-tables").args(&["tables", "include-hidden-tables"])))] + Entity { + #[clap(action, long, help = "Generate entity file of compact format")] + compact_format: bool, + + #[clap(action, long, help = "Generate entity file of expanded format")] + expanded_format: bool, + + #[clap( + action, + long, + help = "Generate entity file for hidden tables (i.e. table name starts with an underscore)" + )] + include_hidden_tables: bool, + + #[clap( + value_parser, + short = 't', + long, + use_value_delimiter = true, + takes_value = true, + help = "Generate entity file for specified tables only (comma separated)" + )] + tables: Option, + + #[clap( + value_parser, + long, + default_value = "1", + help = "The maximum amount of connections to use when connecting to the database." + )] + max_connections: u32, + + #[clap( + value_parser, + short = 'o', + long, + default_value = "./", + help = "Entity file output directory" + )] + output_dir: String, + + #[clap( + value_parser, + short = 's', + long, + env = "DATABASE_SCHEMA", + default_value = "public", + long_help = "Database schema\n \ - For MySQL, this argument is ignored.\n \ - - For PostgreSQL, this argument is optional with default value 'public'.") - .takes_value(true) - .env("DATABASE_SCHEMA"), - ) - .arg( - Arg::with_name("OUTPUT_DIR") - .long("output-dir") - .short("o") - .help("Entity file output directory") - .takes_value(true) - .default_value("./"), - ) - .arg( - Arg::with_name("INCLUDE_HIDDEN_TABLES") - .long("include-hidden-tables") - .help("Generate entity file for hidden tables (i.e. table name starts with an underscore)") - .takes_value(false), - ) - .arg( - Arg::with_name("TABLES") - .long("tables") - .short("t") - .use_delimiter(true) - .help("Generate entity file for specified tables only (comma seperated)") - .takes_value(true) - .conflicts_with("INCLUDE_HIDDEN_TABLES"), - ) - .arg( - Arg::with_name("EXPANDED_FORMAT") - .long("expanded-format") - .help("Generate entity file of expanded format") - .takes_value(false) - .conflicts_with("COMPACT_FORMAT"), - ) - .arg( - Arg::with_name("COMPACT_FORMAT") - .long("compact-format") - .help("Generate entity file of compact format") - .takes_value(false) - .conflicts_with("EXPANDED_FORMAT"), - ) - .arg( - Arg::with_name("WITH_SERDE") - .long("with-serde") - .help("Automatically derive serde Serialize / Deserialize traits for the entity (none, serialize, deserialize, both)") - .takes_value(true) - .default_value("none") - ) - .arg( - Arg::with_name("MAX_CONNECTIONS") - .long("max-connections") - .help("The maximum amount of connections to use when connecting to the database.") - .takes_value(true) - .default_value("1") - ), - ) - .setting(AppSettings::SubcommandRequiredElseHelp); + - For PostgreSQL, this argument is optional with default value 'public'." + )] + database_schema: String, - let arg_migration_dir = Arg::with_name("MIGRATION_DIR") - .long("migration-dir") - .short("d") - .help("Migration script directory") - .takes_value(true) - .default_value("./migration"); - let mut migrate_subcommands = SubCommand::with_name("migrate") - .about("Migration related commands") - .subcommand( - SubCommand::with_name("init") - .about("Initialize migration directory") - .arg(arg_migration_dir.clone()), - ) - .subcommand( - SubCommand::with_name("generate") - .about("Generate a new, empty migration") - .arg( - Arg::with_name("MIGRATION_NAME") - .help("Name of the new migation") - .required(true) - .takes_value(true), - ) - .arg(arg_migration_dir.clone()), - ) - .arg(arg_migration_dir.clone()); - for subcommand in get_subcommands() { - migrate_subcommands = - migrate_subcommands.subcommand(subcommand.arg(arg_migration_dir.clone())); - } + #[clap( + value_parser, + short = 'u', + long, + env = "DATABASE_URL", + help = "Database URL" + )] + database_url: String, - App::new("sea-orm-cli") - .version(env!("CARGO_PKG_VERSION")) - .setting(AppSettings::VersionlessSubcommands) - .subcommand(entity_subcommand) - .subcommand(migrate_subcommands) - .arg( - Arg::with_name("VERBOSE") - .long("verbose") - .short("v") - .help("Show debug messages") - .takes_value(false) - .global(true), - ) - .setting(AppSettings::SubcommandRequiredElseHelp) + #[clap( + value_parser, + long, + default_value = "none", + help = "Automatically derive serde Serialize / Deserialize traits for the entity (none, serialize, deserialize, both)" + )] + with_serde: String, + }, } diff --git a/sea-orm-cli/src/commands.rs b/sea-orm-cli/src/commands.rs index 04a3d3b0b..c82426c7a 100644 --- a/sea-orm-cli/src/commands.rs +++ b/sea-orm-cli/src/commands.rs @@ -1,23 +1,29 @@ use chrono::Local; -use clap::ArgMatches; use regex::Regex; use sea_orm_codegen::{EntityTransformer, OutputFile, WithSerde}; use std::{error::Error, fmt::Display, fs, io::Write, path::Path, process::Command, str::FromStr}; use tracing_subscriber::{prelude::*, EnvFilter}; use url::Url; -pub async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box> { - match matches.subcommand() { - ("entity", Some(args)) => { - let output_dir = args.value_of("OUTPUT_DIR").unwrap(); - let include_hidden_tables = args.is_present("INCLUDE_HIDDEN_TABLES"); - let tables = args - .values_of("TABLES") - .unwrap_or_default() - .collect::>(); - let expanded_format = args.is_present("EXPANDED_FORMAT"); - let with_serde = args.value_of("WITH_SERDE").unwrap(); - if args.is_present("VERBOSE") { +use crate::{GenerateSubcommands, MigrateSubcommands}; + +pub async fn run_generate_command( + command: GenerateSubcommands, + verbose: bool, +) -> Result<(), Box> { + match command { + GenerateSubcommands::Entity { + compact_format: _, + expanded_format, + include_hidden_tables, + tables, + max_connections, + output_dir, + database_schema, + database_url, + with_serde, + } => { + if verbose { let _ = tracing_subscriber::fmt() .with_max_level(tracing::Level::DEBUG) .with_test_writer() @@ -35,18 +41,9 @@ pub async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box) - .transpose()? - .unwrap(); - // The database should be a valid URL that can be parsed // protocol://username:password@host/database_name - let url = Url::parse( - args.value_of("DATABASE_URL") - .expect("No database url could be found"), - )?; + let url = Url::parse(&database_url)?; // Make sure we have all the required url components // @@ -68,6 +65,11 @@ pub async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box t, + _ => "".to_string(), + }; + // Closures for filtering tables let filter_tables = |table: &str| -> bool { if !tables.is_empty() { @@ -76,6 +78,7 @@ pub async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box bool { if include_hidden_tables { true @@ -149,7 +152,7 @@ pub async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box(max_connections, url.as_str()).await?; let schema_discovery = SchemaDiscovery::new(connection, schema); let schema = schema_discovery.discover().await; @@ -165,9 +168,9 @@ pub async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box) -> Result<(), Box unreachable!("You should never see this message"), - }; + } Ok(()) } @@ -201,103 +203,106 @@ where .map_err(Into::into) } -pub fn run_migrate_command(matches: &ArgMatches<'_>) -> Result<(), Box> { - let migrate_subcommand = matches.subcommand(); - // If it's `migrate init` - if let ("init", Some(args)) = migrate_subcommand { - let migration_dir = args.value_of("MIGRATION_DIR").unwrap(); - let migration_dir = match migration_dir.ends_with('/') { - true => migration_dir.to_string(), - false => format!("{}/", migration_dir), - }; - println!("Initializing migration directory..."); - macro_rules! write_file { - ($filename: literal) => { - let fn_content = |content: String| content; - write_file!($filename, $filename, fn_content); - }; - ($filename: literal, $template: literal, $fn_content: expr) => { - let filepath = [&migration_dir, $filename].join(""); - println!("Creating file `{}`", filepath); - let path = Path::new(&filepath); - let prefix = path.parent().unwrap(); - fs::create_dir_all(prefix).unwrap(); - let mut file = fs::File::create(path)?; - let content = include_str!(concat!("../template/migration/", $template)); - let content = $fn_content(content.to_string()); - file.write_all(content.as_bytes())?; +pub fn run_migrate_command( + command: Option, + migration_dir: &str, + verbose: bool, +) -> Result<(), Box> { + match command { + Some(MigrateSubcommands::Init) => { + let migration_dir = match migration_dir.ends_with('/') { + true => migration_dir.to_string(), + false => format!("{}/", migration_dir), }; + println!("Initializing migration directory..."); + macro_rules! write_file { + ($filename: literal) => { + let fn_content = |content: String| content; + write_file!($filename, $filename, fn_content); + }; + ($filename: literal, $template: literal, $fn_content: expr) => { + let filepath = [&migration_dir, $filename].join(""); + println!("Creating file `{}`", filepath); + let path = Path::new(&filepath); + let prefix = path.parent().unwrap(); + fs::create_dir_all(prefix).unwrap(); + let mut file = fs::File::create(path)?; + let content = include_str!(concat!("../template/migration/", $template)); + let content = $fn_content(content.to_string()); + file.write_all(content.as_bytes())?; + }; + } + write_file!("src/lib.rs"); + write_file!("src/m20220101_000001_create_table.rs"); + write_file!("src/main.rs"); + write_file!("Cargo.toml", "_Cargo.toml", |content: String| { + let ver = format!( + "^{}.{}.0", + env!("CARGO_PKG_VERSION_MAJOR"), + env!("CARGO_PKG_VERSION_MINOR") + ); + content.replace("", &ver) + }); + write_file!("README.md"); + println!("Done!"); + // Early exit! + return Ok(()); } - write_file!("src/lib.rs"); - write_file!("src/m20220101_000001_create_table.rs"); - write_file!("src/main.rs"); - write_file!("Cargo.toml", "_Cargo.toml", |content: String| { - let ver = format!( - "^{}.{}.0", - env!("CARGO_PKG_VERSION_MAJOR"), - env!("CARGO_PKG_VERSION_MINOR") - ); - content.replace("", &ver) - }); - write_file!("README.md"); - println!("Done!"); - // Early exit! - return Ok(()); - } else if let ("generate", Some(args)) = migrate_subcommand { - let migration_dir = args.value_of("MIGRATION_DIR").unwrap(); - let migration_name = args.value_of("MIGRATION_NAME").unwrap(); - println!("Generating new migration..."); - - // build new migration filename - let now = Local::now(); - let migration_name = format!( - "m{}_{}", - now.format("%Y%m%d_%H%M%S").to_string(), - migration_name - ); + Some(MigrateSubcommands::Generate { migration_name }) => { + println!("Generating new migration..."); - create_new_migration(&migration_name, migration_dir)?; - update_migrator(&migration_name, migration_dir)?; - return Ok(()); - } - let (subcommand, migration_dir, steps, verbose) = match migrate_subcommand { - // Catch all command with pattern `migrate xxx` - (subcommand, Some(args)) => { - let migration_dir = args.value_of("MIGRATION_DIR").unwrap(); - let steps = args.value_of("NUM_MIGRATION"); - let verbose = args.is_present("VERBOSE"); - (subcommand, migration_dir, steps, verbose) + // build new migration filename + let now = Local::now(); + let migration_name = format!("m{}_{}", now.format("%Y%m%d_%H%M%S"), migration_name); + + create_new_migration(&migration_name, migration_dir)?; + update_migrator(&migration_name, migration_dir)?; + return Ok(()); } - // Catch command `migrate`, this will be treated as `migrate up` _ => { - let migration_dir = matches.value_of("MIGRATION_DIR").unwrap(); - let verbose = matches.is_present("VERBOSE"); - ("up", migration_dir, None, verbose) + let (subcommand, migration_dir, steps, verbose) = match command { + Some(MigrateSubcommands::Fresh) => ("fresh", migration_dir, None, verbose), + Some(MigrateSubcommands::Refresh) => ("refresh", migration_dir, None, verbose), + Some(MigrateSubcommands::Reset) => ("reset", migration_dir, None, verbose), + Some(MigrateSubcommands::Status) => ("status", migration_dir, None, verbose), + Some(MigrateSubcommands::Up { num }) => ("up", migration_dir, Some(num), verbose), + Some(MigrateSubcommands::Down { num }) => { + ("down", migration_dir, Some(num), verbose) + } + _ => ("up", migration_dir, None, verbose), + }; + + // Construct the `--manifest-path` + let manifest_path = if migration_dir.ends_with('/') { + format!("{}Cargo.toml", migration_dir) + } else { + format!("{}/Cargo.toml", migration_dir) + }; + // Construct the arguments that will be supplied to `cargo` command + let mut args = vec![ + "run", + "--manifest-path", + manifest_path.as_str(), + "--", + subcommand, + ]; + + let mut num: String = "".to_string(); + if let Some(steps) = steps { + num = steps.to_string(); + } + if !num.is_empty() { + args.extend(["-n", num.as_str()]) + } + if verbose { + args.push("-v"); + } + // Run migrator CLI on user's behalf + println!("Running `cargo {}`", args.join(" ")); + Command::new("cargo").args(args).spawn()?.wait()?; } - }; - // Construct the `--manifest-path` - let manifest_path = if migration_dir.ends_with('/') { - format!("{}Cargo.toml", migration_dir) - } else { - format!("{}/Cargo.toml", migration_dir) - }; - // Construct the arguments that will be supplied to `cargo` command - let mut args = vec![ - "run", - "--manifest-path", - manifest_path.as_str(), - "--", - subcommand, - ]; - if let Some(steps) = steps { - args.extend(["-n", steps]); - } - if verbose { - args.push("-v"); } - // Run migrator CLI on user's behalf - println!("Running `cargo {}`", args.join(" ")); - Command::new("cargo").args(args).spawn()?.wait()?; + Ok(()) } @@ -310,7 +315,7 @@ fn create_new_migration(migration_name: &str, migration_dir: &str) -> Result<(), let migration_template = include_str!("../template/migration/src/m20220101_000001_create_table.rs"); let migration_content = - migration_template.replace("m20220101_000001_create_table", &migration_name); + migration_template.replace("m20220101_000001_create_table", migration_name); let mut migration_file = fs::File::create(migration_filepath)?; migration_file.write_all(migration_content.as_bytes())?; Ok(()) @@ -327,7 +332,7 @@ fn update_migrator(migration_name: &str, migration_dir: &str) -> Result<(), Box< let mut updated_migrator_content = migrator_content.clone(); // create a backup of the migrator file in case something goes wrong - let migrator_backup_filepath = migrator_filepath.clone().with_file_name("lib.rs.bak"); + let migrator_backup_filepath = migrator_filepath.with_file_name("lib.rs.bak"); fs::copy(&migrator_filepath, &migrator_backup_filepath)?; let mut migrator_file = fs::File::create(&migrator_filepath)?; @@ -348,7 +353,7 @@ fn update_migrator(migration_name: &str, migration_dir: &str) -> Result<(), Box< .map(|migration| format!(" Box::new({}::Migration),", migration)) .collect::>() .join("\n"); - boxed_migrations.push_str("\n"); + boxed_migrations.push('\n'); let boxed_migrations = format!("vec![\n{} ]\n", boxed_migrations); let vec_regex = Regex::new(r"vec!\[[\s\S]+\]\n")?; let updated_migrator_content = vec_regex.replace(&updated_migrator_content, &boxed_migrations); @@ -368,25 +373,30 @@ where #[cfg(test)] mod tests { + use clap::StructOpt; + use super::*; - use crate::cli; - use clap::AppSettings; + use crate::{Cli, Commands}; #[test] #[should_panic( expected = "called `Result::unwrap()` on an `Err` value: RelativeUrlWithoutBase" )] fn test_generate_entity_no_protocol() { - let matches = cli::build_cli() - .setting(AppSettings::NoBinaryName) - .get_matches_from(vec![ - "generate", - "entity", - "--database-url", - "://root:root@localhost:3306/database", - ]); - - smol::block_on(run_generate_command(matches.subcommand().1.unwrap())).unwrap(); + let cli = Cli::parse_from(vec![ + "sea-orm-cli", + "generate", + "entity", + "--database-url", + "://root:root@localhost:3306/database", + ]); + + match cli.command { + Commands::Generate { command } => { + smol::block_on(run_generate_command(command, cli.verbose)).unwrap(); + } + _ => unreachable!(), + } } #[test] @@ -394,16 +404,20 @@ mod tests { expected = "There is no database name as part of the url path: postgresql://root:root@localhost:3306" )] fn test_generate_entity_no_database_section() { - let matches = cli::build_cli() - .setting(AppSettings::NoBinaryName) - .get_matches_from(vec![ - "generate", - "entity", - "--database-url", - "postgresql://root:root@localhost:3306", - ]); - - smol::block_on(run_generate_command(matches.subcommand().1.unwrap())).unwrap(); + let cli = Cli::parse_from(vec![ + "sea-orm-cli", + "generate", + "entity", + "--database-url", + "postgresql://root:root@localhost:3306", + ]); + + match cli.command { + Commands::Generate { command } => { + smol::block_on(run_generate_command(command, cli.verbose)).unwrap(); + } + _ => unreachable!(), + } } #[test] @@ -411,61 +425,77 @@ mod tests { expected = "There is no database name as part of the url path: mysql://root:root@localhost:3306/" )] fn test_generate_entity_no_database_path() { - let matches = cli::build_cli() - .setting(AppSettings::NoBinaryName) - .get_matches_from(vec![ - "generate", - "entity", - "--database-url", - "mysql://root:root@localhost:3306/", - ]); - - smol::block_on(run_generate_command(matches.subcommand().1.unwrap())).unwrap(); + let cli = Cli::parse_from(vec![ + "sea-orm-cli", + "generate", + "entity", + "--database-url", + "mysql://root:root@localhost:3306/", + ]); + + match cli.command { + Commands::Generate { command } => { + smol::block_on(run_generate_command(command, cli.verbose)).unwrap(); + } + _ => unreachable!(), + } } #[test] #[should_panic(expected = "No username was found in the database url")] fn test_generate_entity_no_username() { - let matches = cli::build_cli() - .setting(AppSettings::NoBinaryName) - .get_matches_from(vec![ - "generate", - "entity", - "--database-url", - "mysql://:root@localhost:3306/database", - ]); - - smol::block_on(run_generate_command(matches.subcommand().1.unwrap())).unwrap(); + let cli = Cli::parse_from(vec![ + "sea-orm-cli", + "generate", + "entity", + "--database-url", + "mysql://:root@localhost:3306/database", + ]); + + match cli.command { + Commands::Generate { command } => { + smol::block_on(run_generate_command(command, cli.verbose)).unwrap(); + } + _ => unreachable!(), + } } #[test] #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: PoolTimedOut")] fn test_generate_entity_no_password() { - let matches = cli::build_cli() - .setting(AppSettings::NoBinaryName) - .get_matches_from(vec![ - "generate", - "entity", - "--database-url", - "mysql://root:@localhost:3306/database", - ]); - - smol::block_on(run_generate_command(matches.subcommand().1.unwrap())).unwrap(); + let cli = Cli::parse_from(vec![ + "sea-orm-cli", + "generate", + "entity", + "--database-url", + "mysql://root:@localhost:3306/database", + ]); + + match cli.command { + Commands::Generate { command } => { + smol::block_on(run_generate_command(command, cli.verbose)).unwrap(); + } + _ => unreachable!(), + } } #[test] #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: EmptyHost")] fn test_generate_entity_no_host() { - let matches = cli::build_cli() - .setting(AppSettings::NoBinaryName) - .get_matches_from(vec![ - "generate", - "entity", - "--database-url", - "postgres://root:root@/database", - ]); - - smol::block_on(run_generate_command(matches.subcommand().1.unwrap())).unwrap(); + let cli = Cli::parse_from(vec![ + "sea-orm-cli", + "generate", + "entity", + "--database-url", + "postgres://root:root@/database", + ]); + + match cli.command { + Commands::Generate { command } => { + smol::block_on(run_generate_command(command, cli.verbose)).unwrap(); + } + _ => unreachable!(), + } } #[test] fn test_create_new_migration() { diff --git a/sea-orm-cli/src/lib.rs b/sea-orm-cli/src/lib.rs index d334c9d64..9c552cca6 100644 --- a/sea-orm-cli/src/lib.rs +++ b/sea-orm-cli/src/lib.rs @@ -1,7 +1,6 @@ pub mod cli; #[cfg(feature = "codegen")] pub mod commands; -pub mod migration; pub use cli::*; #[cfg(feature = "codegen")] diff --git a/sea-orm-cli/src/migration.rs b/sea-orm-cli/src/migration.rs deleted file mode 100644 index 41cd507bb..000000000 --- a/sea-orm-cli/src/migration.rs +++ /dev/null @@ -1,49 +0,0 @@ -use clap::{App, AppSettings, Arg, SubCommand}; - -pub fn build_cli() -> App<'static, 'static> { - let mut app = App::new("sea-schema-migration") - .version(env!("CARGO_PKG_VERSION")) - .setting(AppSettings::VersionlessSubcommands) - .arg( - Arg::with_name("VERBOSE") - .long("verbose") - .short("v") - .help("Show debug messages") - .takes_value(false) - .global(true), - ); - for subcommand in get_subcommands() { - app = app.subcommand(subcommand); - } - app -} - -pub fn get_subcommands() -> Vec> { - vec![ - SubCommand::with_name("fresh") - .about("Drop all tables from the database, then reapply all migrations"), - SubCommand::with_name("refresh") - .about("Rollback all applied migrations, then reapply all migrations"), - SubCommand::with_name("reset").about("Rollback all applied migrations"), - SubCommand::with_name("status").about("Check the status of all migrations"), - SubCommand::with_name("up") - .about("Apply pending migrations") - .arg( - Arg::with_name("NUM_MIGRATION") - .long("num") - .short("n") - .help("Number of pending migrations to be applied") - .takes_value(true), - ), - SubCommand::with_name("down") - .about("Rollback applied migrations") - .arg( - Arg::with_name("NUM_MIGRATION") - .long("num") - .short("n") - .help("Number of pending migrations to be rolled back") - .takes_value(true) - .default_value("1"), - ), - ] -} diff --git a/sea-orm-migration/Cargo.toml b/sea-orm-migration/Cargo.toml index 9ca00c742..f834902c2 100644 --- a/sea-orm-migration/Cargo.toml +++ b/sea-orm-migration/Cargo.toml @@ -19,7 +19,7 @@ path = "src/lib.rs" [dependencies] async-trait = { version = "^0.1" } -clap = { version = "^2.33" } +clap = { version = "^3.2", features = ["env", "derive"] } dotenv = { version = "^0.15" } sea-orm = { version = "^0.8.0", path = "../", default-features = false, features = ["macros"] } sea-orm-cli = { version = "^0.8.1", path = "../sea-orm-cli", default-features = false } diff --git a/sea-orm-migration/src/cli.rs b/sea-orm-migration/src/cli.rs index 111c1a5eb..435a53a6a 100644 --- a/sea-orm-migration/src/cli.rs +++ b/sea-orm-migration/src/cli.rs @@ -1,10 +1,10 @@ -use clap::{App, AppSettings, Arg}; +use clap::Parser; use dotenv::dotenv; use std::{fmt::Display, process::exit}; use tracing_subscriber::{prelude::*, EnvFilter}; use sea_orm::{Database, DbConn}; -use sea_orm_cli::migration::get_subcommands; +use sea_orm_cli::MigrateSubcommands; use super::MigratorTrait; @@ -15,27 +15,26 @@ where dotenv().ok(); let url = std::env::var("DATABASE_URL").expect("Environment variable 'DATABASE_URL' not set"); let db = &Database::connect(&url).await.unwrap(); - let app = build_cli(); - get_matches(migrator, db, app).await; + let cli = Cli::parse(); + + run_migrate(migrator, db, cli.command, cli.verbose).await; } -pub async fn get_matches(_: M, db: &DbConn, app: App<'static, 'static>) -where +pub async fn run_migrate( + _: M, + db: &DbConn, + command: Option, + verbose: bool, +) where M: MigratorTrait, { - let matches = app.get_matches(); - let mut verbose = false; - let filter = match matches.subcommand() { - (_, None) => "sea_orm_migration=info", - (_, Some(args)) => match args.is_present("VERBOSE") { - true => { - verbose = true; - "debug" - } - false => "sea_orm_migration=info", - }, + let filter = match verbose { + true => "debug", + false => "sea_orm_migration=info", }; + let filter_layer = EnvFilter::try_new(filter).unwrap(); + if verbose { let fmt_layer = tracing_subscriber::fmt::layer(); tracing_subscriber::registry() @@ -52,44 +51,27 @@ where .with(fmt_layer) .init() }; - match matches.subcommand() { - ("fresh", _) => M::fresh(db).await, - ("refresh", _) => M::refresh(db).await, - ("reset", _) => M::reset(db).await, - ("status", _) => M::status(db).await, - ("up", None) => M::up(db, None).await, - ("down", None) => M::down(db, Some(1)).await, - ("up", Some(args)) => { - let str = args.value_of("NUM_MIGRATION").unwrap_or_default(); - let steps = str.parse().ok(); - M::up(db, steps).await - } - ("down", Some(args)) => { - let str = args.value_of("NUM_MIGRATION").unwrap(); - let steps = str.parse().ok().unwrap_or(1); - M::down(db, Some(steps)).await - } + + match command { + Some(MigrateSubcommands::Fresh) => M::fresh(db).await, + Some(MigrateSubcommands::Refresh) => M::refresh(db).await, + Some(MigrateSubcommands::Reset) => M::reset(db).await, + Some(MigrateSubcommands::Status) => M::status(db).await, + Some(MigrateSubcommands::Up { num }) => M::up(db, Some(num)).await, + Some(MigrateSubcommands::Down { num }) => M::down(db, Some(num)).await, _ => M::up(db, None).await, } .unwrap_or_else(handle_error); } -pub fn build_cli() -> App<'static, 'static> { - let mut app = App::new("sea-orm-migration") - .version(env!("CARGO_PKG_VERSION")) - .setting(AppSettings::VersionlessSubcommands) - .arg( - Arg::with_name("VERBOSE") - .long("verbose") - .short("v") - .help("Show debug messages") - .takes_value(false) - .global(true), - ); - for subcommand in get_subcommands() { - app = app.subcommand(subcommand); - } - app +#[derive(Parser)] +#[clap(version)] +pub struct Cli { + #[clap(action, short = 'v', long, global = true, help = "Show debug messages")] + verbose: bool, + + #[clap(subcommand)] + command: Option, } fn handle_error(error: E)