use std::collections::BTreeMap;
use std::fs;
use std::io::{ErrorKind, Write};
use std::path::Path;
use std::process::{Command, ExitStatus, Stdio};
use regex::Regex;
use serde::Deserialize;
use walkdir::WalkDir;
use crate::git_util;
const ARCHETYPE_FILE: &str = "archetype.json";
const ARCHETYPES_DIR: &str = "archetypes";
const ARCHETYPE_REPO: &str = "https://github.com/codebase-rs/archetypes.git";
const TEMPLATE_DIR: &str = "template";
const CONFIG_REGEX: &str = r"\{\{[A-Z-_]+}}";
#[derive(Deserialize, Clone)]
pub struct Archetype {
pub description: String,
pub author: String,
pub executable: Option<String>,
pub arguments: Option<Vec<String>>,
pub default_config: BTreeMap<String, String>,
}
pub fn find<P: AsRef<Path>>(name: &str, config_dir: P) -> anyhow::Result<Option<Archetype>> {
let archetypes = archetypes(config_dir)?;
Ok(archetypes
.iter()
.find(|(archetype_name, _)| *archetype_name == &name.to_string())
.map(|(_, archetype)| archetype.clone()))
}
pub fn archetypes<P: AsRef<Path>>(config_dir: P) -> anyhow::Result<BTreeMap<String, Archetype>> {
let archetypes_dir = config_dir.as_ref().join(ARCHETYPES_DIR);
if !archetypes_dir.exists() {
git2::Repository::clone(ARCHETYPE_REPO, &archetypes_dir)?;
} else {
let repo = git2::Repository::open(&archetypes_dir)?;
git_util::pull(&repo, "origin", "master")?;
}
let mut archetypes: BTreeMap<String, Archetype> = BTreeMap::new();
for entry in WalkDir::new(&archetypes_dir)
.into_iter()
.filter_map(Result::ok)
.filter(|e| e.file_name().to_str().unwrap() == ARCHETYPE_FILE)
{
let archetypes_dir = archetypes_dir.to_str().unwrap();
let local_path: String = entry
.path()
.to_str()
.unwrap()
.replace(&format!("{}/", archetypes_dir), "")
.replace(&format!("/{}", ARCHETYPE_FILE), "");
let archetype_id = local_path;
let json = fs::read_to_string(&entry.path()).unwrap();
let json: Archetype = serde_json::from_str(&json).unwrap();
archetypes.insert(archetype_id, json);
}
Ok(archetypes)
}
pub fn execute<A: AsRef<Path>, B: AsRef<Path>>(
archetype_id: &str,
config: &BTreeMap<String, String>,
directory: A,
config_dir: B,
) -> anyhow::Result<()> {
let archetype = find(&archetype_id, &config_dir)?.ok_or_else(|| {
anyhow::anyhow!(format!("No archetype with name `{}` found", archetype_id))
})?;
if archetype.executable.is_some() {
execute_wrapper(&archetype, config, directory).map(|_| ())
} else {
execute_template(archetype_id, &archetype, config, directory, &config_dir)
}
}
fn execute_wrapper<P: AsRef<Path>>(
archetype: &Archetype,
config: &BTreeMap<String, String>,
directory: P,
) -> anyhow::Result<ExitStatus> {
let arguments = build_arguments(archetype, config)?;
Command::new(&archetype.executable.as_ref().unwrap())
.args(arguments)
.current_dir(directory)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map_err(|e| {
if let ErrorKind::NotFound = e.kind() {
anyhow::anyhow!(format!(
"Missing executable '{}'",
archetype.executable.as_ref().unwrap()
))
} else {
anyhow::Error::new(e)
}
})
}
fn execute_template<A: AsRef<Path>, B: AsRef<Path>>(
archetype_id: &str,
archetype: &Archetype,
config: &BTreeMap<String, String>,
directory: A,
config_dir: B,
) -> anyhow::Result<()> {
let project_name = config.get("NAME").ok_or_else(|| anyhow::anyhow!(""))?;
let project_directory = directory.as_ref().join(project_name);
let template_dir = config_dir
.as_ref()
.join(ARCHETYPES_DIR)
.join(archetype_id)
.join(TEMPLATE_DIR);
if !template_dir.exists() {
return Err(anyhow::anyhow!(format!(
"Template directory {} does not exist",
template_dir.display()
)));
}
for entry in WalkDir::new(&template_dir)
.into_iter()
.filter_map(Result::ok)
{
let path: &Path = entry.path();
let local_path = path.strip_prefix(&template_dir)?;
let project_path = project_directory.join(local_path);
if entry.file_type().is_dir() {
fs::create_dir_all(&project_path)?;
continue;
}
let content = fs::read_to_string(path)?;
let content = process_template(&content, &archetype, &config)?;
let mut file = fs::File::create(project_path)?;
file.write_all(content.as_bytes())?;
}
Ok(())
}
fn build_arguments(
archetype: &Archetype,
config: &BTreeMap<String, String>,
) -> anyhow::Result<Vec<String>> {
let mut arguments = Vec::new();
if archetype.arguments.is_none() {
return Ok(arguments);
}
let config_regex = Regex::new(CONFIG_REGEX)?;
for arg in archetype.arguments.as_ref().unwrap() {
if config_regex.is_match(&arg) {
let config_name = arg.replace("{{", "").replace("}}", "");
let config_value = config
.get(&config_name)
.or_else(|| archetype.default_config.get(&config_name))
.ok_or_else(|| anyhow::anyhow!("Missing archetype config '{}'", config_name))?;
arguments.push(config_value.clone());
} else {
arguments.push(arg.clone());
}
}
Ok(arguments)
}
fn process_template(
content: &str,
archetype: &Archetype,
config: &BTreeMap<String, String>,
) -> anyhow::Result<String> {
let mut result = content.to_string();
let config_regex = Regex::new(CONFIG_REGEX)?;
for variable in config_regex.find_iter(&content) {
let config_name = variable.as_str().replace("{{", "").replace("}}", "");
let config_value = config
.get(&config_name)
.or_else(|| archetype.default_config.get(&config_name))
.ok_or_else(|| anyhow::anyhow!("Missing archetype config '{}'", config_name))?;
result = result.replace(variable.as_str(), config_value);
}
Ok(result)
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use crate::archetype::{build_arguments, process_template, Archetype};
#[test]
fn test_build_arguments() {
let mut default_config: BTreeMap<String, String> = BTreeMap::new();
default_config.insert("YEAR".to_string(), "2018".to_string());
let archetype = Archetype {
description: "".to_string(),
author: "Aloïs Micard <alois@micard.lu>".to_string(),
executable: Some("cargo".to_string()),
arguments: Some(vec![
"new".to_string(),
"{{NAME}}".to_string(),
"--bin".to_string(),
"--edition".to_string(),
"{{YEAR}}".to_string(),
]),
default_config,
};
let mut config = BTreeMap::new();
let result = build_arguments(&archetype, &config);
assert!(result.is_err());
config.insert("NAME".to_string(), "test".to_string());
let result = build_arguments(&archetype, &config);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
vec!["new", "test", "--bin", "--edition", "2018"]
);
}
#[test]
fn test_process_template() {
let mut default_config: BTreeMap<String, String> = BTreeMap::new();
default_config.insert("FAV_DRINK".to_string(), "beer".to_string());
let archetype = Archetype {
description: "".to_string(),
author: "".to_string(),
executable: None,
arguments: None,
default_config,
};
let content = "Hello, my name is {{NAME}} and I like drinking {{FAV_DRINK}}!";
let mut config: BTreeMap<String, String> = BTreeMap::new();
let result = process_template(&content, &archetype, &config);
assert!(result.is_err());
config.insert("NAME".to_string(), "Aloïs Micard".to_string());
let result = process_template(&content, &archetype, &config);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
"Hello, my name is Aloïs Micard and I like drinking beer!"
)
}
}