use super::Recipe;
use crate::cli::Evoke;
use crate::config::Config;
use crate::content::{File, RecipeItem};
use crate::mkdev_error::{
Error::{self, *},
Subject,
};
use crate::recipe::{OnConflict, instantiate_contents};
use crate::replacer::{InvalidTokenStrategy, ReplaceFmt};
use crate::warning;
use crate::{fs_wrappers, menus};
use std::collections::HashMap;
use std::path::PathBuf;
use std::process::Command;
use rust_i18n::t;
pub struct EvocationCtx {
args: Evoke,
target_dir: PathBuf,
recipes: HashMap<String, Recipe>,
target_recipes: Vec<String>,
resolved_recipes: Vec<Recipe>,
name: String,
}
impl EvocationCtx {
pub fn from_args(args: Evoke, user_recipes: HashMap<String, Recipe>) -> Result<Self, Error> {
if args.interactive {
return Self::from_prompt(args, user_recipes);
}
if args.recipes.is_empty() {
return Err(NoneSpecified {
subject: Subject::Recipes,
});
}
let target_recipes = Recipe::pick_many(&user_recipes, &args.recipes)
.map(|rs| rs.into_iter().map(|r| r.name.clone()).collect::<Vec<_>>())?;
let target_dir = match &args.dir_name {
Some(dir) => PathBuf::from(dir),
None => fs_wrappers::current_dir()?,
};
let mut ctx = Self {
name: Self::unwrap_name(&args),
args,
target_dir,
recipes: user_recipes,
resolved_recipes: Vec::with_capacity(target_recipes.len()),
target_recipes,
};
ctx.resolve_targets()?;
Ok(ctx)
}
pub fn from_prompt(args: Evoke, user_recipes: HashMap<String, Recipe>) -> Result<Self, Error> {
let target_recipes = menus::evoke(&user_recipes)?;
let target_dir = fs_wrappers::current_dir()?;
let mut ctx = Self {
name: Self::unwrap_name(&args),
args,
target_dir,
recipes: user_recipes,
resolved_recipes: Vec::with_capacity(target_recipes.len()),
target_recipes,
};
ctx.resolve_targets()?;
Ok(ctx)
}
pub fn evoke(&mut self) -> Result<(), Error> {
let on_conflict = match self.args.suppress_warnings {
true => OnConflict::Overwrite,
false => OnConflict::Guard,
};
self.resolved_recipes.iter().try_for_each(|recipe| {
instantiate_contents(
&self.target_dir,
&recipe.contents,
on_conflict,
self.args.verbose,
)
.inspect_err(|_| {
warning!(
"{}",
t!("errors.evoke", recipe => recipe.name, target => self.target_dir.display())
)
})
})
}
fn resolve_targets(&mut self) -> Result<(), Error> {
let re = self.init_resolver()?;
self.resolved_recipes.clear();
for recipe_name in &self.target_recipes {
let recipe = self
.recipes
.get(recipe_name)
.expect("These are checked during initialisation.");
self.resolved_recipes.push(Recipe {
contents: resolve_items(&recipe.contents, &re),
..recipe.clone()
})
}
Ok(())
}
fn init_resolver(&self) -> Result<ReplaceFmt, Error> {
let user_subs: HashMap<_, _> = Config::get()?
.subs
.iter()
.map(|(k, v)| match v.as_str() {
"mk::name" => (k.clone(), format!("mk::{}", self.name.clone())),
"mk::dir" => (
k.clone(),
format!("mk::{}", self.target_dir.to_string_lossy()),
),
_ => (k.clone(), v.clone()),
})
.collect();
Ok(ReplaceFmt::new(
user_subs,
("{{", "}}"),
InvalidTokenStrategy::Preserve,
))
}
fn unwrap_name(args: &Evoke) -> String {
match &args.name {
Some(n) => n.clone(),
None => String::from("NAME"),
}
}
}
fn resolve_items(contents: &[RecipeItem], re: &ReplaceFmt) -> Vec<RecipeItem> {
contents
.iter()
.map(|item| match item {
RecipeItem::File(file) => RecipeItem::File(File {
name: re.replace_path_with(run_shell, &file.name),
content: re.replace_with(run_shell, &file.content),
}),
RecipeItem::Directory(dir) => {
RecipeItem::Directory(re.replace_path_with(run_shell, dir))
}
})
.collect()
}
fn run_shell(cmd: &str) -> Option<String> {
if cmd.starts_with("mk::") {
let out = cmd.strip_prefix("mk::").unwrap().to_string();
return Some(out);
}
let output = Command::new("sh").arg("-c").arg(cmd).output().ok();
match output {
Some(output) => {
let mut stdout = String::from_utf8_lossy(&output.stdout).into_owned();
if stdout.ends_with('\n') {
stdout.pop();
}
Some(stdout)
}
None => {
warning!("{}", t!("warnings.child_failed", child => cmd));
None
}
}
}