db-accelerate 2.0.0

Accelerate your databases back and forth through easy to manage migration files.
use std::time::{UNIX_EPOCH, SystemTime};
use std::fmt;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::io::prelude::*;
use std::fs;
use std::fs::File;
use colored::Colorize;
use regex;
use regex::Regex;
use error::Error;

#[derive(Eq, PartialEq, Debug)]
pub struct Motion {
  pub name: String,
  pub add_path: PathBuf,
  pub sub_path: PathBuf,
}

impl fmt::Display for Motion {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    let parent_display = format!("{}", self.add_path.parent().unwrap().display());
    try!(write!(f, "{}", parent_display));
    if parent_display != "" { try!(write!(f, "/")); }
    try!(write!(f, "{}", self.name));
    Ok(())
  }
}

#[derive(Eq, PartialEq, Debug)]
struct Template {
  extension: String,
  add_path: PathBuf,
  sub_path: PathBuf,
}

pub fn find(dir: &Path) -> Result<Vec<Motion>, Error> {
  let template = try!(find_template(dir));
  let motions = try!(find_motions(&template, dir));
  Ok(motions)
}

fn find_template(dir: &Path) -> Result<Template, Error> {
  let paths = try!(find_paths(dir.to_path_buf(), 1));

  // Create the regexi which will match our template file names.
  let add_re = Regex::new(r"^template\.add($|\..+$)").unwrap();
  let sub_re = Regex::new(r"^template\.sub($|\..+$)").unwrap();

  // Find our add and sub paths in our list of paths. Error if we can’t find
  // anything.
  let add_path = try!(
    paths.iter()
    .find(|path| path.file_name().and_then(OsStr::to_str).map(|s| add_re.is_match(s)).unwrap_or(false))
    .ok_or(error!("Add template file was not found for directory '{}'.", dir.display()))
  );

  let sub_path = try!(
    paths.iter()
    .find(|path| path.file_name().and_then(OsStr::to_str).map(|s| sub_re.is_match(s)).unwrap_or(false))
    .ok_or(error!("Sub template file was not found for directory '{}'.", dir.display()))
  );

  // Get both the add extension and the sub extension. We can safely unwrap the
  // file names because part of being an `add_path` or `sub_path` means we
  // already checked that a file name exists.
  let add_ext = add_re.replace_all(add_path.file_name().unwrap().to_str().unwrap(), "$1");
  let sub_ext = sub_re.replace_all(sub_path.file_name().unwrap().to_str().unwrap(), "$1");

  // If the extensions are not equal, there is an error.
  if add_ext != sub_ext {
    return Err(error!("Template extensions for add ('{}') and sub ('{}') do not match.", add_ext, sub_ext))
  }

  // Return our template.
  Ok(Template {
    extension: add_ext,
    add_path: add_path.to_path_buf(),
    sub_path: sub_path.to_path_buf(),
  })
}

fn find_motions(template: &Template, dir: &Path) -> Result<Vec<Motion>, Error> {
  // Get all the file names for our paths for later use. We also make sure we
  // return a tuple. This way we can keep the original path.
  let paths = try!(find_paths(dir.to_path_buf(), 100));
  let paths: Vec<(&PathBuf, &str)> = {
    paths
    .iter()
    .filter_map(|path| path.file_name().and_then(OsStr::to_str).map(|file_name| (path, file_name)))
    .collect()
  };

  // Create the filename regexi for add and sub using the template extension.
  let add_re = Regex::new(&(r"^(.+)\.add".to_owned() + &regex::quote(&template.extension) + "$")).unwrap();
  let sub_re = Regex::new(&(r"^(.+)\.sub".to_owned() + &regex::quote(&template.extension) + "$")).unwrap();
  // Construct a motions accumulator.
  let mut motions: Vec<Motion> = Vec::new();

  // Iterate through all of our paths…
  for &(ref add_path, add_file_name) in paths.iter() {
    // If this path is an add file continue…
    if add_re.is_match(add_file_name) {
      // Get the name and timestamp for this motion.
      let name = add_re.replace_all(add_file_name, "$1");
      // Skip this file if it’s name is `add_file_name`.
      if name == "template" { continue; }
      // Get the sub path with a name that matches our add path. If it does not
      // exist, throw an error.
      let &(ref sub_path, _) = try!(
        paths
        .iter()
        .find(|&&(_, sub_file_name)| sub_re.is_match(sub_file_name) && sub_re.replace_all(sub_file_name, "$1") == name)
        .ok_or(error!("Sub file not found for add file '{}'.", add_path.display()))
      );
      // Add the motion to our accumulator.
      motions.push(Motion {
        name: name,
        add_path: add_path.to_path_buf(),
        sub_path: sub_path.to_path_buf(),
      });
    }
  }

  // Sort our motions by *file* name.
  motions.sort_by(|a, b| a.name.cmp(&b.name));

  // Return all of our motions.
  Ok(motions)
}

fn find_paths(path: PathBuf, recurse: u8) -> Result<Vec<PathBuf>, Error> {
  // If the path is a directory let’s recursively go through every entry and
  // rerun our `discover_all` function.
  if try!(fs::metadata(&path)).is_dir() {
    // If we have ended our recursion, just return an empty vec.
    if recurse == 0 {
      return Ok(vec![])
    }
    // Create a new paths vec.
    let mut paths: Vec<PathBuf> = Vec::new();
    // Loop through the directory…
    for entry in try!(fs::read_dir(&path)) {
      // Get all the paths from this entry path by recursively calling the
      // function.
      let mut next_paths = try!(find_paths(try!(entry).path(), recurse - 1));
      // Append these next paths to our top level motions vec.
      paths.append(&mut next_paths);
    }
    // Return all of our motions.
    Ok(paths)
  }
  // Otherwise our path is not a directory and we should return a singleton
  // vector of the path.
  else {
    Ok(vec![path])
  }
}

// TODO: Tests.
pub fn create(dir: &Path, name: &str) -> Result<(), Error> {
  // Find the template for the directory we are creating a motion for.
  let template = try!(find_template(dir));
  // Add a timestamp to the name of the new motion.
  let name = add_timestamp_to_name(name);
  // Construct the add path and the sub path using our name and the template’s
  // extension in addition to the directory the motions are in.
  let mut add_path = PathBuf::new();
  let mut sub_path = PathBuf::new();
  add_path.push(&dir);
  add_path.push(format!("{}.add{}", name, template.extension));
  sub_path.push(&dir);
  sub_path.push(format!("{}.sub{}", name, template.extension));
  // Copy the add file and the sub file to their new locations and log some
  // pretty things.
  try!(copy_file(&template.add_path, &add_path));
  println!("{} {}", "Create".green().bold(), add_path.display());
  try!(copy_file(&template.sub_path, &sub_path));
  println!("{} {}", "Create".green().bold(), sub_path.display());
  Ok(())
}

fn add_timestamp_to_name(name: &str) -> String {
  // Get all of the name’s segments (path segments).
  let mut segments = name.split('/').map(String::from).collect::<Vec<String>>();
  // Get the index of the last segment.
  let last_i = segments.len() - 1;
  // Get the timestamp in minutes.
  let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs().checked_div(60).unwrap();
  // Add the timestamp to the last segment.
  segments[last_i] = format!("{}", timestamp) + "-" + &segments[last_i];
  // Rejoin the segments with a slash and return the new name.
  segments.join("/")
}

fn copy_file(in_path: &Path, out_path: &Path) -> Result<(), Error> {
  // Open the file we will copy.
  let mut in_file = try!(File::open(in_path));
  // Read the contents of our in file to a string.
  let mut contents = String::new();
  try!(in_file.read_to_string(&mut contents));
  // Create the directory for our out path.
  if let Some(parent) = out_path.parent() { try!(fs::create_dir_all(parent)); }
  // Create our out file.
  let mut out_file = try!(File::create(out_path));
  // Write all of the contents of our in file to the out file.
  try!(out_file.write_all(contents.as_bytes()));
  Ok(())
}

#[cfg(test)]
mod tests {
  use std::path::{Path, PathBuf};
  use super::{find_paths, find, Motion, find_template, Template};

  fn pb(path: &str) -> PathBuf {
    Path::new(path).to_path_buf()
  }

  #[test]
  fn test_find_paths() {
    assert_eq!(find_paths(pb("tests/fixtures/nested"), 100).unwrap(), vec![
      pb("tests/fixtures/nested/234567-bar.add"),
      pb("tests/fixtures/nested/234567-bar.sub"),
      pb("tests/fixtures/nested/a/345678-baz.add"),
      pb("tests/fixtures/nested/a/345678-baz.sub"),
      pb("tests/fixtures/nested/b/123456-foo.add"),
      pb("tests/fixtures/nested/b/123456-foo.sub"),
      pb("tests/fixtures/nested/b/c/456789-qux.add"),
      pb("tests/fixtures/nested/b/c/456789-qux.sub"),
      pb("tests/fixtures/nested/template.add"),
      pb("tests/fixtures/nested/template.sub"),
    ]);
  }

  #[test]
  fn test_find_paths_limit_recurse_1() {
    assert_eq!(find_paths(pb("tests/fixtures/nested"), 1).unwrap(), vec![
      pb("tests/fixtures/nested/234567-bar.add"),
      pb("tests/fixtures/nested/234567-bar.sub"),
      pb("tests/fixtures/nested/template.add"),
      pb("tests/fixtures/nested/template.sub"),
    ]);
  }

  #[test]
  fn test_find_paths_limit_recurse_2() {
    assert_eq!(find_paths(pb("tests/fixtures/nested"), 2).unwrap(), vec![
      pb("tests/fixtures/nested/234567-bar.add"),
      pb("tests/fixtures/nested/234567-bar.sub"),
      pb("tests/fixtures/nested/a/345678-baz.add"),
      pb("tests/fixtures/nested/a/345678-baz.sub"),
      pb("tests/fixtures/nested/b/123456-foo.add"),
      pb("tests/fixtures/nested/b/123456-foo.sub"),
      pb("tests/fixtures/nested/template.add"),
      pb("tests/fixtures/nested/template.sub"),
    ]);
  }

  #[test]
  fn test_fixtures_basic() {
    assert_eq!(find(Path::new("tests/fixtures/basic")).unwrap(), vec![
      Motion {
        name: "123456-foo".to_string(),
        add_path: pb("tests/fixtures/basic/123456-foo.add"),
        sub_path: pb("tests/fixtures/basic/123456-foo.sub"),
      },
      Motion {
        name: "234567-bar".to_string(),
        add_path: pb("tests/fixtures/basic/234567-bar.add"),
        sub_path: pb("tests/fixtures/basic/234567-bar.sub"),
      },
    ]);
  }

  #[test]
  fn test_template_basic() {
    assert_eq!(find_template(Path::new("tests/fixtures/basic")).unwrap(), Template {
      extension: "".to_string(),
      add_path: pb("tests/fixtures/basic/template.add"),
      sub_path: pb("tests/fixtures/basic/template.sub"),
    });
  }

  #[test]
  fn test_fixtures_nested() {
    assert_eq!(find(Path::new("tests/fixtures/nested")).unwrap(), vec![
      Motion {
        name: "123456-foo".to_string(),
        add_path: pb("tests/fixtures/nested/b/123456-foo.add"),
        sub_path: pb("tests/fixtures/nested/b/123456-foo.sub"),
      },
      Motion {
        name: "234567-bar".to_string(),
        add_path: pb("tests/fixtures/nested/234567-bar.add"),
        sub_path: pb("tests/fixtures/nested/234567-bar.sub"),
      },
      Motion {
        name: "345678-baz".to_string(),
        add_path: pb("tests/fixtures/nested/a/345678-baz.add"),
        sub_path: pb("tests/fixtures/nested/a/345678-baz.sub"),
      },
      Motion {
        name: "456789-qux".to_string(),
        add_path: pb("tests/fixtures/nested/b/c/456789-qux.add"),
        sub_path: pb("tests/fixtures/nested/b/c/456789-qux.sub"),
      },
    ]);
  }

  #[test]
  fn test_template_nested() {
    assert_eq!(find_template(Path::new("tests/fixtures/nested")).unwrap(), Template {
      extension: "".to_string(),
      add_path: pb("tests/fixtures/nested/template.add"),
      sub_path: pb("tests/fixtures/nested/template.sub"),
    });
  }

  #[test]
  fn test_fixtures_extension() {
    assert_eq!(find(Path::new("tests/fixtures/extension")).unwrap(), vec![
      Motion {
        name: "123456-foo".to_string(),
        add_path: pb("tests/fixtures/extension/123456-foo.add.sql"),
        sub_path: pb("tests/fixtures/extension/123456-foo.sub.sql"),
      },
      Motion {
        name: "234567-bar".to_string(),
        add_path: pb("tests/fixtures/extension/234567-bar.add.sql"),
        sub_path: pb("tests/fixtures/extension/234567-bar.sub.sql"),
      },
    ]);
  }

  #[test]
  fn test_template_extension() {
    assert_eq!(find_template(Path::new("tests/fixtures/extension")).unwrap(), Template {
      extension: ".sql".to_string(),
      add_path: pb("tests/fixtures/extension/template.add.sql"),
      sub_path: pb("tests/fixtures/extension/template.sub.sql"),
    });
  }

  #[test]
  fn test_fixtures_bad_templateless() {
    assert!(find(Path::new("tests/fixtures/bad/templateless")).is_err());
  }

  #[test]
  fn test_bad_names() {
    assert!(find(Path::new("tests/fixtures/bad/names")).is_err());
  }

  #[test]
  fn test_motion_display_current_dir() {
    assert_eq!(format!("{}", Motion {
      name: "foo".to_string(),
      add_path: pb("foo.add"),
      sub_path: pb("foo.sub"),
    }), "foo");
  }

  #[test]
  fn test_motion_display_nested_dir() {
    assert_eq!(format!("{}", Motion {
      name: "foo".to_string(),
      add_path: pb("hello/world/foo.add"),
      sub_path: pb("hello/world/foo.sub"),
    }), "hello/world/foo");
  }

  #[test]
  fn test_motion_display_extension() {
    assert_eq!(format!("{}", Motion {
      name: "foo".to_string(),
      add_path: pb("foo.add.txt"),
      sub_path: pb("foo.sub.txt"),
    }), "foo");
  }
}