use crate::{error::KickError, *};
use clap::ArgMatches;
use git2;
use leg;
use regex::Regex;
use sane;
use serde::Deserialize;
use std::borrow::Cow;
use std::{
env, fs,
path::{Path, PathBuf},
};
pub trait BuildFrom {
fn build_from(&self, root: &PathBuf, config: &Config) -> Result<(), KickError>;
}
pub trait ValidateInput<'a> {
fn validate_input(&self, id: ID<'a>) -> Result<(), KickError>;
}
pub enum ID<'a> {
None,
Name(&'a str),
}
enum TemplateBuilder<'a> {
TemplateFile(PathBuf),
TemplateUrl(&'a Cow<'a, str>),
}
impl<'a> TemplateBuilder<'a> {
fn new(input: &'a Cow<'a, str>) -> Self {
if Regex::new(consts::LIFTOFF_URL_REGEX)
.unwrap()
.is_match(input.as_ref())
{
TemplateBuilder::TemplateUrl(input)
} else {
TemplateBuilder::TemplateFile(PathBuf::from(&input.as_ref()))
}
}
fn build(&self, file: &PathBuf) -> Result<(), KickError> {
use TemplateBuilder::*;
match self {
TemplateFile(tmpl_file) => files::copy(&tmpl_file, &file)?,
TemplateUrl(url) => web::download(url, &file)?,
}
Ok(())
}
}
#[derive(Debug, Deserialize)]
pub struct Config<'a> {
#[serde(skip)]
pub project: &'a str,
pub language: Cow<'a, str>,
#[serde(skip)]
pub git: bool,
#[serde(borrow)]
pub base_command: Option<Cow<'a, str>>,
#[serde(borrow)]
pub author: Option<Cow<'a, str>>,
#[serde(borrow)]
pub root: Option<Cow<'a, str>>,
#[serde(borrow)]
pub license: Option<Cow<'a, str>>,
#[serde(borrow)]
pub ci: Option<CiConfig<'a>>,
#[serde(borrow)]
pub directories: Option<Vec<DirectoryConfig<'a>>>,
#[serde(borrow)]
pub files: Option<Vec<FileConfig<'a>>>,
}
#[derive(Debug, Deserialize)]
pub struct FileConfig<'a> {
pub name: Cow<'a, str>,
#[serde(borrow)]
pub template: Option<Cow<'a, str>>,
#[serde(borrow)]
pub content: Option<Cow<'a, str>>,
}
#[derive(Debug, Deserialize)]
pub struct DirectoryConfig<'a> {
pub name: Cow<'a, str>,
#[serde(borrow)]
pub files: Option<Vec<FileConfig<'a>>>,
#[serde(borrow)]
pub file: Option<FileConfig<'a>>,
}
#[derive(Debug, Deserialize)]
pub struct GitConfig<'a> {
pub enable: bool,
#[serde(borrow)]
pub file: Option<FileConfig<'a>>,
}
#[derive(Debug, Deserialize)]
pub struct CiConfig<'a> {
pub name: Cow<'a, str>,
#[serde(borrow)]
pub template: Option<Cow<'a, str>>,
}
pub struct License<'a> {
pub name: &'a str,
pub author: &'a Option<Cow<'a, str>>,
}
impl<'a> Config<'a> {
pub fn new(contents: &'a String) -> Result<Self, KickError> {
let mut config: Config = sane::from_str(&contents)?;
config.git = true;
Ok(config)
}
pub fn from_args(contents: &'a String, matches: &'a ArgMatches) -> Result<Self, KickError> {
let mut config: Config = Config::new(&contents)?;
config.project = &matches.value_of("name").unwrap(); config.project.validate_input(ID::Name("project name"))?;
if let Some(license) = matches.value_of("license") {
config.license.validate_input(ID::Name("license"))?;
config.license = Some(license.into());
}
if let Some(author) = matches.value_of("author") {
config.author.validate_input(ID::Name("author"))?;
config.author = Some(author.into());
}
if let Some(root) = matches.value_of("root") {
config.root.validate_input(ID::Name("root"))?;
config.root = Some(root.into());
}
if let Some(ci) = matches.value_of("ci") {
config.ci = Some(CiConfig {
name: ci.into(),
template: None,
});
}
match matches.occurrences_of("nogit") {
1 => {
config.git = false;
},
_ => {}
}
Ok(config)
}
pub fn build(&mut self) -> Result<(), KickError> {
let mut root_path = env::current_dir()?.join(&self.project);
if let Some(root) = &self.root {
root_path = PathBuf::from(root.as_ref()).join(&self.project);
}
pinfo!("Kickstarting \"{}\" in {}", self.project, root_path.display());
self.build_from(&root_path, &self)?;
psuccess!("Done");
Ok(())
}
}
impl<'a> BuildFrom for Config<'a> {
fn build_from(&self, root: &PathBuf, _config: &Config) -> Result<(), KickError> {
if let Some(ref directories) = &self.directories {
for dir in directories.iter() {
dir.build_from(&root, &self)?;
}
}
if let Some(ref files) = &self.files {
for file in files.iter() {
file.build_from(&root, &self)?;
}
}
if let Some(ref ci) = &self.ci {
ci.build_from(&root, &self)?;
}
if let Some(ref license) = &self.license {
let license = License {
name: &license,
author: &self.author,
};
license.build_from(&root, &_config)?;
}
if self.git == true {
let _ = git2::Repository::init(&root.as_path())?;
psuccess!("Created directory {}", root.join(".git").display());
web::download_gitignore(&self.language, &root.join(".gitignore"))?;
}
Ok(())
}
}
impl<'a> BuildFrom for DirectoryConfig<'a> {
fn build_from(&self, root: &PathBuf, config: &Config) -> Result<(), KickError> {
self.validate_input(ID::None)?;
let dir_path = variables::path_from_variables(&root, &self.name, &config)?;
files::mkdir(&dir_path)?;
if let Some(ref files) = &self.files {
for file in files.iter() {
file.build_from(&dir_path, &config)?;
}
}
if let Some(ref file) = &self.file {
file.build_from(&dir_path, &config)?;
}
Ok(())
}
}
impl<'a> ValidateInput<'a> for DirectoryConfig<'a> {
fn validate_input(&self, _id: ID<'a>) -> Result<(), KickError> {
if self.name.is_empty() {
return Err(KickError::EmptyFieldError("directory::file::name".to_string()))
}
Ok(())
}
}
impl<'a> BuildFrom for FileConfig<'a> {
fn build_from(&self, root: &PathBuf, config: &Config) -> Result<(), KickError> {
self.validate_input(ID::None)?;
if !Path::exists(root) {
files::mkdir(&root)?;
}
let file_path = variables::path_from_variables(&root, &self.name, &config)?;
if let Some(templ_str) = &self.template {
let template = TemplateBuilder::new(templ_str);
template.build(&file_path)?;
} else {
files::touch(&file_path)?;
}
if let Some(content) = &self.content {
let content_vars_replaced = variables::replace_variables(&content, &config)?;
files::echo(&content_vars_replaced, &file_path)?;
}
Ok(())
}
}
impl<'a> ValidateInput<'a> for FileConfig<'a> {
fn validate_input(&self, _id: ID) -> Result<(), KickError> {
if self.name.is_empty() {
return Err(KickError::EmptyFieldError("file::name".to_string()))
}
if let Some(ref template) = &self.template {
if template.is_empty() {
return Err(KickError::EmptyFieldError("file::template".to_string()))
}
}
if let Some(ref content) = &self.content {
if content.is_empty() {
return Err(KickError::EmptyFieldError("file::content".to_string()))
}
}
Ok(())
}
}
impl<'a> BuildFrom for CiConfig<'a> {
fn build_from(&self, root: &PathBuf, _config: &Config) -> Result<(), KickError> {
self.validate_input(ID::None)?;
let mut file;
match self.name.as_ref() {
"circleci" => {
let dir = root.join(".circleci");
file = dir.join("config.yml");
files::mkdir(&dir)?;
}
"travisci" => file = root.join(".travis.yml"),
"appveyor" => file = root.join("appveyor.yml"),
_ => return Err(KickError::InvalidCIError(self.name.to_string())),
}
if let Some(ref templ_str) = &self.template {
let template = TemplateBuilder::new(templ_str);
template.build(&file)?;
}
Ok(())
}
}
impl<'a> ValidateInput<'a> for CiConfig<'a> {
fn validate_input(&self, _id: ID) -> Result<(), KickError> {
if self.name.is_empty() {
return Err(KickError::EmptyFieldError("ci::name".to_string()))
}
if let Some(ref template) = &self.template {
if template.is_empty() {
return Err(KickError::EmptyFieldError("ci::template".to_string()))
}
}
Ok(())
}
}
impl<'a> BuildFrom for License<'a> {
fn build_from(&self, root: &PathBuf, _config: &Config) -> Result<(), KickError> {
self.validate_input(ID::None)?;
let license_path = Path::new(consts::LIFTOFF_HOME)
.join("licenses/")
.join(self.name)
.with_extension("txt");
let target_path = root.join("LICENSE");
if self.name.starts_with("mit") || self.name.starts_with("bsd") {
if self.author.is_none() {
return Err(KickError::LicenseAuthorError(self.name.to_string()))
}
let content = {
let original_text = fs::read_to_string(license_path)?.into();
variables::replace_variables(&original_text, &_config)?
};
files::echo(&content, &target_path)?;
} else {
if !Path::exists(license_path.as_path()) {
return Err(KickError::InvalidLicenseError(self.name.to_string()))
}
files::copy(&license_path, &target_path)?;
}
Ok(())
}
}
impl<'a> ValidateInput<'a> for License<'a> {
fn validate_input(&self, _id: ID) -> Result<(), KickError> {
if self.name.is_empty() {
return Err(KickError::EmptyFieldError("license::name".to_string()))
}
if let Some(ref author) = &self.author {
if author.is_empty() {
return Err(KickError::EmptyFieldError("license::author".to_string()))
}
}
Ok(())
}
}
impl<'a> ValidateInput<'a> for &'a str {
fn validate_input(&self, id: ID) -> Result<(), KickError> {
use ID::*;
match id {
Name(name) => {
if self.is_empty() {
Err(KickError::EmptyFieldError(name.to_string()))
} else {
Ok(())
}
}
_ => {
panic!("Can't use this without variable ID");
}
}
}
}
impl<'a> ValidateInput<'a> for Option<Cow<'a, str>> {
fn validate_input(&self, id: ID<'a>) -> Result<(), KickError> {
use ID::*;
match id {
Name(name) => {
if let Some(cow_s) = self {
if cow_s.is_empty() {
Err(KickError::EmptyFieldError(name.to_string()))
} else {
Ok(())
}
} else {
Ok(())
}
},
_ => {
panic!("Can't use this without variable ID");
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_input() {
assert!("".validate_input(ID::Name("empty")).is_err());
assert!("hi".validate_input(ID::Name("hi")).is_ok());
assert!(Some(Cow::Borrowed("hi")).validate_input(ID::Name("Holy cow")).is_ok());
assert!(Some(Cow::Borrowed("")).validate_input(ID::Name("Holy cow empty")).is_err());
}
}