use std::collections::BTreeMap;
use std::fs;
use std::fs::File;
use std::io::Read;
use std::iter::repeat;
use std::path::Path;
use errors::{Result, ResultExt};
use regex::Regex;
#[derive(Debug, PartialEq)]
pub enum Direction {
Up,
Down,
}
impl ToString for Direction {
fn to_string(&self) -> String {
match *self {
Direction::Up => "up".to_owned(),
Direction::Down => "down".to_owned(),
}
}
}
#[derive(Debug)]
pub struct MigrationFile {
pub content: Option<String>,
pub direction: Direction,
pub number: i32,
pub filename: String,
pub name: String,
}
#[derive(Debug)]
pub struct Migration {
pub up: Option<MigrationFile>,
pub down: Option<MigrationFile>,
}
pub type Migrations = BTreeMap<i32, Migration>;
impl MigrationFile {
fn new(filename: &str, name: &str, number: i32, direction: Direction) -> MigrationFile {
MigrationFile {
content: None,
filename: filename.to_owned(),
number,
name: name.to_owned(),
direction,
}
}
}
pub fn create_migration(path: &Path, slug: &str, number: i32) -> Result<()> {
let fixed_slug = slug.replace(" ", "_");
let filename_up = get_filename(&fixed_slug, number, Direction::Up);
parse_filename(&filename_up)?;
let filename_down = get_filename(&fixed_slug, number, Direction::Down);
parse_filename(&filename_down)?;
println!("Creating {}", filename_up);
File::create(path.join(filename_up.clone()))
.chain_err(|| format!("Failed to create {}", filename_up))?;
println!("Creating {}", filename_down);
File::create(path.join(filename_down.clone()))
.chain_err(|| format!("Failed to create {}", filename_down))?;
Ok(())
}
fn get_filename(slug: &str, number: i32, direction: Direction) -> String {
let num = number.to_string();
let filler = repeat("0").take(4 - num.len()).collect::<String>();
filler + &num + "." + slug + "." + &direction.to_string() + ".sql"
}
pub fn read_migration_files(path: &Path) -> Result<Migrations> {
let mut btreemap: Migrations = BTreeMap::new();
for entry in fs::read_dir(path).chain_err(|| format!("Failed to open {:?}", path))? {
let entry = entry.unwrap();
let info = match parse_filename(entry.file_name().to_str().unwrap()) {
Ok(info) => info,
Err(_) => continue,
};
let mut file =
File::open(entry.path()).chain_err(|| format!("Failed to open {:?}", entry.path()))?;
let mut content = String::new();
file.read_to_string(&mut content)?;
let migration_file = MigrationFile {
content: Some(content),
..info
};
let migration_number = migration_file.number;
let mut migration = match btreemap.remove(&migration_number) {
None => Migration {
up: None,
down: None,
},
Some(m) => m,
};
match migration_file.direction {
Direction::Up if migration.up.is_none() => {
migration.up = Some(migration_file);
}
Direction::Down if migration.down.is_none() => {
migration.down = Some(migration_file);
}
_ => {
bail!(
"There are multiple migrations with number {}",
migration_number
)
}
};
btreemap.insert(migration_number, migration);
}
let mut index = 1;
for (number, migration) in &btreemap {
if index != *number {
bail!("Files for migration {} are missing", index);
}
if migration.up.is_none() || migration.down.is_none() {
bail!("Migration {} is missing its up or down file", index);
}
index += 1;
}
Ok(btreemap)
}
fn parse_filename(filename: &str) -> Result<MigrationFile> {
let re =
Regex::new(r"^(?P<number>[0-9]{4})\.(?P<name>[_0-9a-zA-Z]*)\.(?P<direction>up|down)\.sql$")
.unwrap();
let caps = match re.captures(filename) {
None => bail!("File {} has an invalid filename", filename),
Some(c) => c,
};
let number = caps
.name("number")
.unwrap()
.as_str()
.parse::<i32>()
.unwrap();
let name = caps.name("name").unwrap().as_str();
let direction = if caps.name("direction").unwrap().as_str() == "up" {
Direction::Up
} else {
Direction::Down
};
Ok(MigrationFile::new(filename, name, number, direction))
}
#[cfg(test)]
mod tests {
use super::{get_filename, parse_filename, read_migration_files, Direction};
use std::fs::File;
use std::io::prelude::*;
use std::path::PathBuf;
use tempdir::TempDir;
fn create_file(path: &PathBuf, filename: &str) {
let mut new_path = path.clone();
new_path.push(filename);
let mut f = File::create(new_path.to_str().unwrap()).unwrap();
f.write_all(b"Hello, world!").unwrap();
}
#[test]
fn test_parse_good_filename() {
let result = parse_filename("0001.tests.up.sql").unwrap();
assert_eq!(result.number, 1);
assert_eq!(result.name, "tests");
assert_eq!(result.direction, Direction::Up);
}
#[test]
fn test_parse_bad_filename_format() {
let result = parse_filename("0001_tests.up.sql");
assert_eq!(result.is_ok(), false);
}
#[test]
fn test_get_filename_ok() {
let result = get_filename("initial", 1, Direction::Up);
assert_eq!(result, "0001.initial.up.sql");
}
#[test]
fn test_parse_good_migrations_directory() {
let pathbuf = TempDir::new("migrations").unwrap().into_path();
create_file(&pathbuf, "0001.tests.up.sql");
create_file(&pathbuf, "0001.tests.down.sql");
create_file(&pathbuf, "0002.tests_second.up.sql");
create_file(&pathbuf, "0002.tests_second.down.sql");
let migrations = read_migration_files(pathbuf.as_path());
assert_eq!(migrations.is_ok(), true);
}
#[test]
fn test_parse_missing_migrations_directory() {
let pathbuf = TempDir::new("migrations").unwrap().into_path();
create_file(&pathbuf, "0001.tests.up.sql");
create_file(&pathbuf, "0001.tests.down.sql");
create_file(&pathbuf, "0002.tests_second.up.sql");
let migrations = read_migration_files(pathbuf.as_path());
assert_eq!(migrations.is_err(), true);
}
#[test]
fn test_parse_skipping_migrations_directory() {
let pathbuf = TempDir::new("migrations").unwrap().into_path();
create_file(&pathbuf, "0001.tests.up.sql");
create_file(&pathbuf, "0001.tests.down.sql");
create_file(&pathbuf, "0003.tests_second.up.sql");
create_file(&pathbuf, "0003.tests_second.down.sql");
let migrations = read_migration_files(pathbuf.as_path());
assert_eq!(migrations.is_err(), true);
}
#[test]
fn test_two_migrations_same_number() {
let tests: &[&[&str]] = &[
&["0001.a.up.sql", "0001.a.down.sql", "0001.b.up.sql"],
&["0001.a.up.sql", "0001.a.down.sql", "0001.b.down.sql"],
&[
"0001.a.up.sql",
"0001.a.down.sql",
"0001.b.up.sql",
"0001.b.down.sql",
],
];
for files in tests {
let pathbuf = TempDir::new("migrations").unwrap().into_path();
for file in files.iter() {
create_file(&pathbuf, file);
}
let migrations = read_migration_files(pathbuf.as_path());
assert_eq!(migrations.is_err(), true);
}
}
}