Bump clap version to 3.2 (#706)

* bump clap to 3.1.17

sea-orm-migration: bump clap version to 3.1.17

sea-orm-cli: use clap derive API instead of builder API

sea-orm-migration: use clap derive

* Fix clippy warnings

* Migration CLI verbose with default value

* bump clap to 3.2

Co-authored-by: Thanh Van <tvt@smonv.com>
Co-authored-by: Billy Chan <ccw.billy.123@gmail.com>
This commit is contained in:
smonv 2022-06-26 19:52:33 +07:00 committed by GitHub
parent ca3809a6d3
commit 580fa90023
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 425 additions and 418 deletions

View File

@ -29,7 +29,7 @@ path = "src/bin/sea.rs"
required-features = ["codegen"] required-features = ["codegen"]
[dependencies] [dependencies]
clap = { version = "^2.33.3" } clap = { version = "^3.2", features = ["env", "derive"] }
dotenv = { version = "^0.15" } dotenv = { version = "^0.15" }
async-std = { version = "^1.9", features = [ "attributes", "tokio1" ] } async-std = { version = "^1.9", features = [ "attributes", "tokio1" ] }
sea-orm-codegen = { version = "^0.8.0", path = "../sea-orm-codegen", optional = true } sea-orm-codegen = { version = "^0.8.0", path = "../sea-orm-codegen", optional = true }

View File

@ -1,17 +1,24 @@
use clap::StructOpt;
use dotenv::dotenv; 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_std::main]
async fn main() { async fn main() {
dotenv().ok(); dotenv().ok();
let matches = cli::build_cli().get_matches(); let cli = Cli::parse();
let verbose = cli.verbose;
match matches.subcommand() { match cli.command {
("generate", Some(matches)) => run_generate_command(matches) Commands::Generate { command } => {
run_generate_command(command, verbose)
.await .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), .unwrap_or_else(handle_error),
("migrate", Some(matches)) => run_migrate_command(matches).unwrap_or_else(handle_error),
_ => unreachable!("You should never see this message"),
} }
} }

View File

@ -1,19 +1,26 @@
//! COPY FROM bin/main.rs //! COPY FROM bin/main.rs
use clap::StructOpt;
use dotenv::dotenv; 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_std::main]
async fn main() { async fn main() {
dotenv().ok(); dotenv().ok();
let matches = cli::build_cli().get_matches(); let cli = Cli::parse();
let verbose = cli.verbose;
match matches.subcommand() { match cli.command {
("generate", Some(matches)) => run_generate_command(matches) Commands::Generate { command } => {
run_generate_command(command, verbose)
.await .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), .unwrap_or_else(handle_error),
("migrate", Some(matches)) => run_migrate_command(matches).unwrap_or_else(handle_error),
_ => unreachable!("You should never see this message"),
} }
} }

View File

@ -1,129 +1,160 @@
use crate::migration::get_subcommands; use clap::{ArgGroup, Parser, Subcommand};
use clap::{App, AppSettings, Arg, SubCommand};
pub fn build_cli() -> App<'static, 'static> { #[derive(Parser, Debug)]
let entity_subcommand = SubCommand::with_name("generate") #[clap(version)]
.about("Codegen related commands") pub struct Cli {
.setting(AppSettings::VersionlessSubcommands) #[clap(action, global = true, short, long, help = "Show debug messages")]
.subcommand( pub verbose: bool,
SubCommand::with_name("entity")
.about("Generate entity") #[clap(subcommand)]
.arg( pub command: Commands,
Arg::with_name("DATABASE_URL") }
.long("database-url")
.short("u") #[derive(Subcommand, PartialEq, Debug)]
.help("Database URL") pub enum Commands {
.takes_value(true) #[clap(about = "Codegen related commands")]
.required(true) #[clap(arg_required_else_help = true)]
.env("DATABASE_URL"), Generate {
) #[clap(subcommand)]
.arg( command: GenerateSubcommands,
Arg::with_name("DATABASE_SCHEMA") },
.long("database-schema") #[clap(about = "Migration related commands")]
.short("s") Migrate {
.help("Database schema") #[clap(
.long_help("Database schema\n \ value_parser,
global = true,
short = 'd',
long,
help = "Migration script directory",
default_value = "./migration"
)]
migration_dir: String,
#[clap(subcommand)]
command: Option<MigrateSubcommands>,
},
}
#[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<String>,
#[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 MySQL, this argument is ignored.\n \
- For PostgreSQL, this argument is optional with default value 'public'.") - For PostgreSQL, this argument is optional with default value 'public'."
.takes_value(true) )]
.env("DATABASE_SCHEMA"), database_schema: String,
)
.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);
let arg_migration_dir = Arg::with_name("MIGRATION_DIR") #[clap(
.long("migration-dir") value_parser,
.short("d") short = 'u',
.help("Migration script directory") long,
.takes_value(true) env = "DATABASE_URL",
.default_value("./migration"); help = "Database URL"
let mut migrate_subcommands = SubCommand::with_name("migrate") )]
.about("Migration related commands") database_url: String,
.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()));
}
App::new("sea-orm-cli") #[clap(
.version(env!("CARGO_PKG_VERSION")) value_parser,
.setting(AppSettings::VersionlessSubcommands) long,
.subcommand(entity_subcommand) default_value = "none",
.subcommand(migrate_subcommands) help = "Automatically derive serde Serialize / Deserialize traits for the entity (none, serialize, deserialize, both)"
.arg( )]
Arg::with_name("VERBOSE") with_serde: String,
.long("verbose") },
.short("v")
.help("Show debug messages")
.takes_value(false)
.global(true),
)
.setting(AppSettings::SubcommandRequiredElseHelp)
} }

View File

@ -1,23 +1,29 @@
use chrono::Local; use chrono::Local;
use clap::ArgMatches;
use regex::Regex; use regex::Regex;
use sea_orm_codegen::{EntityTransformer, OutputFile, WithSerde}; use sea_orm_codegen::{EntityTransformer, OutputFile, WithSerde};
use std::{error::Error, fmt::Display, fs, io::Write, path::Path, process::Command, str::FromStr}; use std::{error::Error, fmt::Display, fs, io::Write, path::Path, process::Command, str::FromStr};
use tracing_subscriber::{prelude::*, EnvFilter}; use tracing_subscriber::{prelude::*, EnvFilter};
use url::Url; use url::Url;
pub async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box<dyn Error>> { use crate::{GenerateSubcommands, MigrateSubcommands};
match matches.subcommand() {
("entity", Some(args)) => { pub async fn run_generate_command(
let output_dir = args.value_of("OUTPUT_DIR").unwrap(); command: GenerateSubcommands,
let include_hidden_tables = args.is_present("INCLUDE_HIDDEN_TABLES"); verbose: bool,
let tables = args ) -> Result<(), Box<dyn Error>> {
.values_of("TABLES") match command {
.unwrap_or_default() GenerateSubcommands::Entity {
.collect::<Vec<_>>(); compact_format: _,
let expanded_format = args.is_present("EXPANDED_FORMAT"); expanded_format,
let with_serde = args.value_of("WITH_SERDE").unwrap(); include_hidden_tables,
if args.is_present("VERBOSE") { tables,
max_connections,
output_dir,
database_schema,
database_url,
with_serde,
} => {
if verbose {
let _ = tracing_subscriber::fmt() let _ = tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG) .with_max_level(tracing::Level::DEBUG)
.with_test_writer() .with_test_writer()
@ -35,18 +41,9 @@ pub async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box<dy
.try_init(); .try_init();
} }
let max_connections = args
.value_of("MAX_CONNECTIONS")
.map(str::parse::<u32>)
.transpose()?
.unwrap();
// The database should be a valid URL that can be parsed // The database should be a valid URL that can be parsed
// protocol://username:password@host/database_name // protocol://username:password@host/database_name
let url = Url::parse( let url = Url::parse(&database_url)?;
args.value_of("DATABASE_URL")
.expect("No database url could be found"),
)?;
// Make sure we have all the required url components // Make sure we have all the required url components
// //
@ -68,6 +65,11 @@ pub async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box<dy
} }
} }
let tables = match tables {
Some(t) => t,
_ => "".to_string(),
};
// Closures for filtering tables // Closures for filtering tables
let filter_tables = |table: &str| -> bool { let filter_tables = |table: &str| -> bool {
if !tables.is_empty() { if !tables.is_empty() {
@ -76,6 +78,7 @@ pub async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box<dy
true true
}; };
let filter_hidden_tables = |table: &str| -> bool { let filter_hidden_tables = |table: &str| -> bool {
if include_hidden_tables { if include_hidden_tables {
true true
@ -149,7 +152,7 @@ pub async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box<dy
use sea_schema::postgres::discovery::SchemaDiscovery; use sea_schema::postgres::discovery::SchemaDiscovery;
use sqlx::Postgres; use sqlx::Postgres;
let schema = args.value_of("DATABASE_SCHEMA").unwrap_or("public"); let schema = &database_schema;
let connection = connect::<Postgres>(max_connections, url.as_str()).await?; let connection = connect::<Postgres>(max_connections, url.as_str()).await?;
let schema_discovery = SchemaDiscovery::new(connection, schema); let schema_discovery = SchemaDiscovery::new(connection, schema);
let schema = schema_discovery.discover().await; let schema = schema_discovery.discover().await;
@ -165,9 +168,9 @@ pub async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box<dy
}; };
let output = EntityTransformer::transform(table_stmts)? let output = EntityTransformer::transform(table_stmts)?
.generate(expanded_format, WithSerde::from_str(with_serde).unwrap()); .generate(expanded_format, WithSerde::from_str(&with_serde).unwrap());
let dir = Path::new(output_dir); let dir = Path::new(&output_dir);
fs::create_dir_all(dir)?; fs::create_dir_all(dir)?;
for OutputFile { name, content } in output.files.iter() { for OutputFile { name, content } in output.files.iter() {
@ -184,8 +187,7 @@ pub async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box<dy
.wait()?; .wait()?;
} }
} }
_ => unreachable!("You should never see this message"), }
};
Ok(()) Ok(())
} }
@ -201,11 +203,13 @@ where
.map_err(Into::into) .map_err(Into::into)
} }
pub fn run_migrate_command(matches: &ArgMatches<'_>) -> Result<(), Box<dyn Error>> { pub fn run_migrate_command(
let migrate_subcommand = matches.subcommand(); command: Option<MigrateSubcommands>,
// If it's `migrate init` migration_dir: &str,
if let ("init", Some(args)) = migrate_subcommand { verbose: bool,
let migration_dir = args.value_of("MIGRATION_DIR").unwrap(); ) -> Result<(), Box<dyn Error>> {
match command {
Some(MigrateSubcommands::Init) => {
let migration_dir = match migration_dir.ends_with('/') { let migration_dir = match migration_dir.ends_with('/') {
true => migration_dir.to_string(), true => migration_dir.to_string(),
false => format!("{}/", migration_dir), false => format!("{}/", migration_dir),
@ -243,38 +247,31 @@ pub fn run_migrate_command(matches: &ArgMatches<'_>) -> Result<(), Box<dyn Error
println!("Done!"); println!("Done!");
// Early exit! // Early exit!
return Ok(()); return Ok(());
} else if let ("generate", Some(args)) = migrate_subcommand { }
let migration_dir = args.value_of("MIGRATION_DIR").unwrap(); Some(MigrateSubcommands::Generate { migration_name }) => {
let migration_name = args.value_of("MIGRATION_NAME").unwrap();
println!("Generating new migration..."); println!("Generating new migration...");
// build new migration filename // build new migration filename
let now = Local::now(); let now = Local::now();
let migration_name = format!( let migration_name = format!("m{}_{}", now.format("%Y%m%d_%H%M%S"), migration_name);
"m{}_{}",
now.format("%Y%m%d_%H%M%S").to_string(),
migration_name
);
create_new_migration(&migration_name, migration_dir)?; create_new_migration(&migration_name, migration_dir)?;
update_migrator(&migration_name, migration_dir)?; update_migrator(&migration_name, migration_dir)?;
return Ok(()); 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)
}
// Catch command `migrate`, this will be treated as `migrate up`
_ => { _ => {
let migration_dir = matches.value_of("MIGRATION_DIR").unwrap(); let (subcommand, migration_dir, steps, verbose) = match command {
let verbose = matches.is_present("VERBOSE"); Some(MigrateSubcommands::Fresh) => ("fresh", migration_dir, None, verbose),
("up", 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` // Construct the `--manifest-path`
let manifest_path = if migration_dir.ends_with('/') { let manifest_path = if migration_dir.ends_with('/') {
format!("{}Cargo.toml", migration_dir) format!("{}Cargo.toml", migration_dir)
@ -289,8 +286,13 @@ pub fn run_migrate_command(matches: &ArgMatches<'_>) -> Result<(), Box<dyn Error
"--", "--",
subcommand, subcommand,
]; ];
let mut num: String = "".to_string();
if let Some(steps) = steps { if let Some(steps) = steps {
args.extend(["-n", steps]); num = steps.to_string();
}
if !num.is_empty() {
args.extend(["-n", num.as_str()])
} }
if verbose { if verbose {
args.push("-v"); args.push("-v");
@ -298,6 +300,9 @@ pub fn run_migrate_command(matches: &ArgMatches<'_>) -> Result<(), Box<dyn Error
// Run migrator CLI on user's behalf // Run migrator CLI on user's behalf
println!("Running `cargo {}`", args.join(" ")); println!("Running `cargo {}`", args.join(" "));
Command::new("cargo").args(args).spawn()?.wait()?; Command::new("cargo").args(args).spawn()?.wait()?;
}
}
Ok(()) Ok(())
} }
@ -310,7 +315,7 @@ fn create_new_migration(migration_name: &str, migration_dir: &str) -> Result<(),
let migration_template = let migration_template =
include_str!("../template/migration/src/m20220101_000001_create_table.rs"); include_str!("../template/migration/src/m20220101_000001_create_table.rs");
let migration_content = 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)?; let mut migration_file = fs::File::create(migration_filepath)?;
migration_file.write_all(migration_content.as_bytes())?; migration_file.write_all(migration_content.as_bytes())?;
Ok(()) Ok(())
@ -327,7 +332,7 @@ fn update_migrator(migration_name: &str, migration_dir: &str) -> Result<(), Box<
let mut updated_migrator_content = migrator_content.clone(); let mut updated_migrator_content = migrator_content.clone();
// create a backup of the migrator file in case something goes wrong // 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)?; fs::copy(&migrator_filepath, &migrator_backup_filepath)?;
let mut migrator_file = fs::File::create(&migrator_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)) .map(|migration| format!(" Box::new({}::Migration),", migration))
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("\n"); .join("\n");
boxed_migrations.push_str("\n"); boxed_migrations.push('\n');
let boxed_migrations = format!("vec![\n{} ]\n", boxed_migrations); let boxed_migrations = format!("vec![\n{} ]\n", boxed_migrations);
let vec_regex = Regex::new(r"vec!\[[\s\S]+\]\n")?; let vec_regex = Regex::new(r"vec!\[[\s\S]+\]\n")?;
let updated_migrator_content = vec_regex.replace(&updated_migrator_content, &boxed_migrations); let updated_migrator_content = vec_regex.replace(&updated_migrator_content, &boxed_migrations);
@ -368,25 +373,30 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use clap::StructOpt;
use super::*; use super::*;
use crate::cli; use crate::{Cli, Commands};
use clap::AppSettings;
#[test] #[test]
#[should_panic( #[should_panic(
expected = "called `Result::unwrap()` on an `Err` value: RelativeUrlWithoutBase" expected = "called `Result::unwrap()` on an `Err` value: RelativeUrlWithoutBase"
)] )]
fn test_generate_entity_no_protocol() { fn test_generate_entity_no_protocol() {
let matches = cli::build_cli() let cli = Cli::parse_from(vec![
.setting(AppSettings::NoBinaryName) "sea-orm-cli",
.get_matches_from(vec![
"generate", "generate",
"entity", "entity",
"--database-url", "--database-url",
"://root:root@localhost:3306/database", "://root:root@localhost:3306/database",
]); ]);
smol::block_on(run_generate_command(matches.subcommand().1.unwrap())).unwrap(); match cli.command {
Commands::Generate { command } => {
smol::block_on(run_generate_command(command, cli.verbose)).unwrap();
}
_ => unreachable!(),
}
} }
#[test] #[test]
@ -394,16 +404,20 @@ mod tests {
expected = "There is no database name as part of the url path: postgresql://root:root@localhost:3306" expected = "There is no database name as part of the url path: postgresql://root:root@localhost:3306"
)] )]
fn test_generate_entity_no_database_section() { fn test_generate_entity_no_database_section() {
let matches = cli::build_cli() let cli = Cli::parse_from(vec![
.setting(AppSettings::NoBinaryName) "sea-orm-cli",
.get_matches_from(vec![
"generate", "generate",
"entity", "entity",
"--database-url", "--database-url",
"postgresql://root:root@localhost:3306", "postgresql://root:root@localhost:3306",
]); ]);
smol::block_on(run_generate_command(matches.subcommand().1.unwrap())).unwrap(); match cli.command {
Commands::Generate { command } => {
smol::block_on(run_generate_command(command, cli.verbose)).unwrap();
}
_ => unreachable!(),
}
} }
#[test] #[test]
@ -411,61 +425,77 @@ mod tests {
expected = "There is no database name as part of the url path: mysql://root:root@localhost:3306/" expected = "There is no database name as part of the url path: mysql://root:root@localhost:3306/"
)] )]
fn test_generate_entity_no_database_path() { fn test_generate_entity_no_database_path() {
let matches = cli::build_cli() let cli = Cli::parse_from(vec![
.setting(AppSettings::NoBinaryName) "sea-orm-cli",
.get_matches_from(vec![
"generate", "generate",
"entity", "entity",
"--database-url", "--database-url",
"mysql://root:root@localhost:3306/", "mysql://root:root@localhost:3306/",
]); ]);
smol::block_on(run_generate_command(matches.subcommand().1.unwrap())).unwrap(); match cli.command {
Commands::Generate { command } => {
smol::block_on(run_generate_command(command, cli.verbose)).unwrap();
}
_ => unreachable!(),
}
} }
#[test] #[test]
#[should_panic(expected = "No username was found in the database url")] #[should_panic(expected = "No username was found in the database url")]
fn test_generate_entity_no_username() { fn test_generate_entity_no_username() {
let matches = cli::build_cli() let cli = Cli::parse_from(vec![
.setting(AppSettings::NoBinaryName) "sea-orm-cli",
.get_matches_from(vec![
"generate", "generate",
"entity", "entity",
"--database-url", "--database-url",
"mysql://:root@localhost:3306/database", "mysql://:root@localhost:3306/database",
]); ]);
smol::block_on(run_generate_command(matches.subcommand().1.unwrap())).unwrap(); match cli.command {
Commands::Generate { command } => {
smol::block_on(run_generate_command(command, cli.verbose)).unwrap();
}
_ => unreachable!(),
}
} }
#[test] #[test]
#[should_panic(expected = "called `Result::unwrap()` on an `Err` value: PoolTimedOut")] #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: PoolTimedOut")]
fn test_generate_entity_no_password() { fn test_generate_entity_no_password() {
let matches = cli::build_cli() let cli = Cli::parse_from(vec![
.setting(AppSettings::NoBinaryName) "sea-orm-cli",
.get_matches_from(vec![
"generate", "generate",
"entity", "entity",
"--database-url", "--database-url",
"mysql://root:@localhost:3306/database", "mysql://root:@localhost:3306/database",
]); ]);
smol::block_on(run_generate_command(matches.subcommand().1.unwrap())).unwrap(); match cli.command {
Commands::Generate { command } => {
smol::block_on(run_generate_command(command, cli.verbose)).unwrap();
}
_ => unreachable!(),
}
} }
#[test] #[test]
#[should_panic(expected = "called `Result::unwrap()` on an `Err` value: EmptyHost")] #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: EmptyHost")]
fn test_generate_entity_no_host() { fn test_generate_entity_no_host() {
let matches = cli::build_cli() let cli = Cli::parse_from(vec![
.setting(AppSettings::NoBinaryName) "sea-orm-cli",
.get_matches_from(vec![
"generate", "generate",
"entity", "entity",
"--database-url", "--database-url",
"postgres://root:root@/database", "postgres://root:root@/database",
]); ]);
smol::block_on(run_generate_command(matches.subcommand().1.unwrap())).unwrap(); match cli.command {
Commands::Generate { command } => {
smol::block_on(run_generate_command(command, cli.verbose)).unwrap();
}
_ => unreachable!(),
}
} }
#[test] #[test]
fn test_create_new_migration() { fn test_create_new_migration() {

View File

@ -1,7 +1,6 @@
pub mod cli; pub mod cli;
#[cfg(feature = "codegen")] #[cfg(feature = "codegen")]
pub mod commands; pub mod commands;
pub mod migration;
pub use cli::*; pub use cli::*;
#[cfg(feature = "codegen")] #[cfg(feature = "codegen")]

View File

@ -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<App<'static, 'static>> {
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"),
),
]
}

View File

@ -19,7 +19,7 @@ path = "src/lib.rs"
[dependencies] [dependencies]
async-trait = { version = "^0.1" } async-trait = { version = "^0.1" }
clap = { version = "^2.33" } clap = { version = "^3.2", features = ["env", "derive"] }
dotenv = { version = "^0.15" } dotenv = { version = "^0.15" }
sea-orm = { version = "^0.8.0", path = "../", default-features = false, features = ["macros"] } 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 } sea-orm-cli = { version = "^0.8.1", path = "../sea-orm-cli", default-features = false }

View File

@ -1,10 +1,10 @@
use clap::{App, AppSettings, Arg}; use clap::Parser;
use dotenv::dotenv; use dotenv::dotenv;
use std::{fmt::Display, process::exit}; use std::{fmt::Display, process::exit};
use tracing_subscriber::{prelude::*, EnvFilter}; use tracing_subscriber::{prelude::*, EnvFilter};
use sea_orm::{Database, DbConn}; use sea_orm::{Database, DbConn};
use sea_orm_cli::migration::get_subcommands; use sea_orm_cli::MigrateSubcommands;
use super::MigratorTrait; use super::MigratorTrait;
@ -15,27 +15,26 @@ where
dotenv().ok(); dotenv().ok();
let url = std::env::var("DATABASE_URL").expect("Environment variable 'DATABASE_URL' not set"); let url = std::env::var("DATABASE_URL").expect("Environment variable 'DATABASE_URL' not set");
let db = &Database::connect(&url).await.unwrap(); let db = &Database::connect(&url).await.unwrap();
let app = build_cli(); let cli = Cli::parse();
get_matches(migrator, db, app).await;
run_migrate(migrator, db, cli.command, cli.verbose).await;
} }
pub async fn get_matches<M>(_: M, db: &DbConn, app: App<'static, 'static>) pub async fn run_migrate<M>(
where _: M,
db: &DbConn,
command: Option<MigrateSubcommands>,
verbose: bool,
) where
M: MigratorTrait, M: MigratorTrait,
{ {
let matches = app.get_matches(); let filter = match verbose {
let mut verbose = false; true => "debug",
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", false => "sea_orm_migration=info",
},
}; };
let filter_layer = EnvFilter::try_new(filter).unwrap(); let filter_layer = EnvFilter::try_new(filter).unwrap();
if verbose { if verbose {
let fmt_layer = tracing_subscriber::fmt::layer(); let fmt_layer = tracing_subscriber::fmt::layer();
tracing_subscriber::registry() tracing_subscriber::registry()
@ -52,44 +51,27 @@ where
.with(fmt_layer) .with(fmt_layer)
.init() .init()
}; };
match matches.subcommand() {
("fresh", _) => M::fresh(db).await, match command {
("refresh", _) => M::refresh(db).await, Some(MigrateSubcommands::Fresh) => M::fresh(db).await,
("reset", _) => M::reset(db).await, Some(MigrateSubcommands::Refresh) => M::refresh(db).await,
("status", _) => M::status(db).await, Some(MigrateSubcommands::Reset) => M::reset(db).await,
("up", None) => M::up(db, None).await, Some(MigrateSubcommands::Status) => M::status(db).await,
("down", None) => M::down(db, Some(1)).await, Some(MigrateSubcommands::Up { num }) => M::up(db, Some(num)).await,
("up", Some(args)) => { Some(MigrateSubcommands::Down { num }) => M::down(db, Some(num)).await,
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
}
_ => M::up(db, None).await, _ => M::up(db, None).await,
} }
.unwrap_or_else(handle_error); .unwrap_or_else(handle_error);
} }
pub fn build_cli() -> App<'static, 'static> { #[derive(Parser)]
let mut app = App::new("sea-orm-migration") #[clap(version)]
.version(env!("CARGO_PKG_VERSION")) pub struct Cli {
.setting(AppSettings::VersionlessSubcommands) #[clap(action, short = 'v', long, global = true, help = "Show debug messages")]
.arg( verbose: bool,
Arg::with_name("VERBOSE")
.long("verbose") #[clap(subcommand)]
.short("v") command: Option<MigrateSubcommands>,
.help("Show debug messages")
.takes_value(false)
.global(true),
);
for subcommand in get_subcommands() {
app = app.subcommand(subcommand);
}
app
} }
fn handle_error<E>(error: E) fn handle_error<E>(error: E)