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));
let add_re = Regex::new(r"^template\.add($|\..+$)").unwrap();
let sub_re = Regex::new(r"^template\.sub($|\..+$)").unwrap();
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()))
);
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 add_ext != sub_ext {
return Err(error!("Template extensions for add ('{}') and sub ('{}') do not match.", add_ext, sub_ext))
}
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> {
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()
};
let add_re = Regex::new(&(r"^(.+)\.add".to_owned() + ®ex::quote(&template.extension) + "$")).unwrap();
let sub_re = Regex::new(&(r"^(.+)\.sub".to_owned() + ®ex::quote(&template.extension) + "$")).unwrap();
let mut motions: Vec<Motion> = Vec::new();
for &(ref add_path, add_file_name) in paths.iter() {
if add_re.is_match(add_file_name) {
let name = add_re.replace_all(add_file_name, "$1");
if name == "template" { continue; }
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()))
);
motions.push(Motion {
name: name,
add_path: add_path.to_path_buf(),
sub_path: sub_path.to_path_buf(),
});
}
}
motions.sort_by(|a, b| a.name.cmp(&b.name));
Ok(motions)
}
fn find_paths(path: PathBuf, recurse: u8) -> Result<Vec<PathBuf>, Error> {
if try!(fs::metadata(&path)).is_dir() {
if recurse == 0 {
return Ok(vec![])
}
let mut paths: Vec<PathBuf> = Vec::new();
for entry in try!(fs::read_dir(&path)) {
let mut next_paths = try!(find_paths(try!(entry).path(), recurse - 1));
paths.append(&mut next_paths);
}
Ok(paths)
}
else {
Ok(vec![path])
}
}
pub fn create(dir: &Path, name: &str) -> Result<(), Error> {
let template = try!(find_template(dir));
let name = add_timestamp_to_name(name);
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));
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 {
let mut segments = name.split('/').map(String::from).collect::<Vec<String>>();
let last_i = segments.len() - 1;
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs().checked_div(60).unwrap();
segments[last_i] = format!("{}", timestamp) + "-" + &segments[last_i];
segments.join("/")
}
fn copy_file(in_path: &Path, out_path: &Path) -> Result<(), Error> {
let mut in_file = try!(File::open(in_path));
let mut contents = String::new();
try!(in_file.read_to_string(&mut contents));
if let Some(parent) = out_path.parent() { try!(fs::create_dir_all(parent)); }
let mut out_file = try!(File::create(out_path));
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");
}
}