Skip to content

Commit

Permalink
CLI command to generate a new migration (#656)
Browse files Browse the repository at this point in the history
* feat(cli): add 'migration generate' subcommand

This subcommend will create a new, empty migration.

* feat(deps): add chrono crate

This crate will allow me to fetch the current date and time required for
generating the migration filename.

* feat(cli): generate migration filename

* feat(cli): read template, replace migration name

* feat(cli): write modified content to file

* feat(deps): add regex crate

Allows me to parse the lib.rs file containing the migrator logic.

* fix(cli): add missing chrono import

* feat(cli): mod declaration for new migration

This modifies the existing migator file, adding a module declaration for
the newly generated migration.

* feat(cli): regenerate migration vector

* feat(cli): write updated migrator file to disk

This completes updating the migrator file with the new migration
information.

* docs(cli): additional docstring

* refactor(cli): move logic into functions

* test(cli): create new migration happy path

* test(cli): update migrator happy path

* fix(cli): dedicated tmp dir for test

This avoids conflicts with the other tests.

* style(cli): align generated code with cargofmt

As suggested by @billy1624 in the review of #656.

* feat(cli): harden regex against extra spaces

As suggested by @billy1624 in the review of #656.

Co-authored-by: Billy Chan <[email protected]>
  • Loading branch information
viktorbahr and billy1624 authored May 9, 2022
1 parent b8214b2 commit 3518acf
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 0 deletions.
2 changes: 2 additions & 0 deletions sea-orm-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ sqlx = { version = "^0.5", default-features = false, features = [ "mysql", "post
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing = { version = "0.1" }
url = "^2.2"
chrono = "0.4"
regex = "1"

[dev-dependencies]
smol = "1.2.5"
Expand Down
11 changes: 11 additions & 0 deletions sea-orm-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,17 @@ pub fn build_cli() -> App<'static, 'static> {
.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 =
Expand Down
129 changes: 129 additions & 0 deletions sea-orm-cli/src/commands.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
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 url::Url;
Expand Down Expand Up @@ -220,6 +222,22 @@ pub fn run_migrate_command(matches: &ArgMatches<'_>) -> Result<(), Box<dyn Error
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
);

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`
Expand Down Expand Up @@ -262,6 +280,63 @@ pub fn run_migrate_command(matches: &ArgMatches<'_>) -> Result<(), Box<dyn Error
Ok(())
}

fn create_new_migration(migration_name: &str, migration_dir: &str) -> Result<(), Box<dyn Error>> {
let migration_filepath = Path::new(migration_dir)
.join("src")
.join(format!("{}.rs", &migration_name));
println!("Creating migration file `{}`", migration_filepath.display());
// TODO: make OS agnostic
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);
let mut migration_file = fs::File::create(migration_filepath)?;
migration_file.write_all(migration_content.as_bytes())?;
Ok(())
}

fn update_migrator(migration_name: &str, migration_dir: &str) -> Result<(), Box<dyn Error>> {
let migrator_filepath = Path::new(migration_dir).join("src").join("lib.rs");
println!(
"Adding migration `{}` to `{}`",
migration_name,
migrator_filepath.display()
);
let migrator_content = fs::read_to_string(&migrator_filepath)?;
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.bkp");
fs::copy(&migrator_filepath, &migrator_backup_filepath)?;
let mut migrator_file = fs::File::create(&migrator_filepath)?;

// find existing mod declarations, add new line
let mod_regex = Regex::new(r"mod\s+(?P<name>m\d{8}_\d{6}_\w+);")?;
let mods: Vec<_> = mod_regex.captures_iter(&migrator_content).collect();
let mods_end = mods.last().unwrap().get(0).unwrap().end() + 1;
updated_migrator_content.insert_str(mods_end, format!("mod {};\n", migration_name).as_str());

// build new vector from declared migration modules
let mut migrations: Vec<&str> = mods
.iter()
.map(|cap| cap.name("name").unwrap().as_str())
.collect();
migrations.push(migration_name);
let mut boxed_migrations = migrations
.iter()
.map(|migration| format!(" Box::new({}::Migration),", migration))
.collect::<Vec<String>>()
.join("\n");
boxed_migrations.push_str("\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);

migrator_file.write_all(updated_migrator_content.as_bytes())?;
fs::remove_file(&migrator_backup_filepath)?;
Ok(())
}

pub fn handle_error<E>(error: E)
where
E: Display,
Expand Down Expand Up @@ -371,4 +446,58 @@ mod tests {

smol::block_on(run_generate_command(matches.subcommand().1.unwrap())).unwrap();
}
#[test]
fn test_create_new_migration() {
let migration_name = "test_name";
let migration_dir = "/tmp/sea_orm_cli_test_new_migration/";
fs::create_dir_all(format!("{}src", migration_dir)).unwrap();
create_new_migration(migration_name, migration_dir).unwrap();
let migration_filepath = Path::new(migration_dir)
.join("src")
.join(format!("{}.rs", migration_name));
assert!(migration_filepath.exists());
let migration_content = fs::read_to_string(migration_filepath).unwrap();
let migration_content =
migration_content.replace(&migration_name, "m20220101_000001_create_table");
assert_eq!(
&migration_content,
include_str!("../template/migration/src/m20220101_000001_create_table.rs")
);
fs::remove_dir_all("/tmp/sea_orm_cli_test_new_migration/").unwrap();
}

#[test]
fn test_update_migrator() {
let migration_name = "test_name";
let migration_dir = "/tmp/sea_orm_cli_test_update_migrator/";
fs::create_dir_all(format!("{}src", migration_dir)).unwrap();
let migrator_filepath = Path::new(migration_dir).join("src").join("lib.rs");
fs::copy("./template/migration/src/lib.rs", &migrator_filepath).unwrap();
update_migrator(migration_name, migration_dir).unwrap();
assert!(&migrator_filepath.exists());
let migrator_content = fs::read_to_string(&migrator_filepath).unwrap();
let mod_regex = Regex::new(r"mod (?P<name>\w+);").unwrap();
let migrations: Vec<&str> = mod_regex
.captures_iter(&migrator_content)
.map(|cap| cap.name("name").unwrap().as_str())
.collect();
assert_eq!(migrations.len(), 2);
assert_eq!(
*migrations.first().unwrap(),
"m20220101_000001_create_table"
);
assert_eq!(migrations.last().unwrap(), &migration_name);
let boxed_regex = Regex::new(r"Box::new\((?P<name>\S+)::Migration\)").unwrap();
let migrations: Vec<&str> = boxed_regex
.captures_iter(&migrator_content)
.map(|cap| cap.name("name").unwrap().as_str())
.collect();
assert_eq!(migrations.len(), 2);
assert_eq!(
*migrations.first().unwrap(),
"m20220101_000001_create_table"
);
assert_eq!(migrations.last().unwrap(), &migration_name);
fs::remove_dir_all("/tmp/sea_orm_cli_test_update_migrator/").unwrap();
}
}

0 comments on commit 3518acf

Please sign in to comment.