use std::{path::PathBuf, str::FromStr}; use anyhow::Context; use clap::{Arg, ArgMatches, Command}; use crate::{cli::CuddleCli, model::CuddleVariable}; const DESTINATION: &str = "destination is the output path of the template once done, but default .tmpl is stripped and the normal file extension is used. this can be overwritten if a file path is entered instead. I.e. (/some/file/name.txt)"; const TEMPLATE_FILE: &str = "template-file is the input file path of the .tmpl file (or inferred) that you would like to render"; pub fn build_command(root_cmd: Command) -> Command { root_cmd.subcommand( Command::new("render_template") .about("renders a jinja compatible template") .args(&[ Arg::new("template-file") .alias("template") .short('t') .long("template-file") .required(true) .action(clap::ArgAction::Set) .long_help(TEMPLATE_FILE), Arg::new("destination") .alias("dest") .short('d') .long("destination") .required(true) .action(clap::ArgAction::Set) .long_help(DESTINATION), Arg::new("extra-var") .long("extra-var") .required(false) .action(clap::ArgAction::Append), ]), ) } pub struct RenderTemplateCommand { variables: Vec, template_file: PathBuf, destination: PathBuf, } impl RenderTemplateCommand { pub fn from_matches(matches: &ArgMatches, cli: CuddleCli) -> anyhow::Result { let template_file = matches .get_one::("template-file") .ok_or(anyhow::anyhow!("template-file was not found")) .and_then(get_path_buf_and_check_exists)?; let destination = matches .get_one::("destination") .ok_or(anyhow::anyhow!("destination was not found")) .and_then(get_path_buf_and_check_dir_exists) .and_then(RenderTemplateCommand::transform_extension) .context("failed to access dest directory")?; let mut extra_vars: Vec = if let Some(extra_vars) = matches.get_many::("extra-var") { let mut vars = Vec::with_capacity(extra_vars.len()); for var in extra_vars.into_iter() { let parts: Vec<&str> = var.split('=').collect(); if parts.len() != 2 { return Err(anyhow::anyhow!("extra-var: is not set correctly: {}", var)); } vars.push(CuddleVariable::new(parts[0], parts[1])); } vars } else { vec![] }; extra_vars.append(&mut cli.variables.clone()); Ok(Self { variables: extra_vars, template_file, destination, }) } pub fn execute(self) -> anyhow::Result<()> { // Prepare context let mut context = tera::Context::new(); for var in self.variables { context.insert(var.name.to_lowercase().replace([' ', '-'], "_"), &var.value) } // Load source template let source = std::fs::read_to_string(self.template_file)?; let output = tera::Tera::one_off(source.as_str(), &context, false)?; if let Some(parent) = self.destination.parent() { std::fs::create_dir_all(parent)?; } // Put template in final destination std::fs::write(&self.destination, output).context(format!( "failed to write to destination: {}", &self.destination.display(), ))?; log::info!( "finished writing template to: {}", &self.destination.to_string_lossy() ); Ok(()) } fn transform_extension(template_path: PathBuf) -> anyhow::Result { if template_path.is_file() { let ext = template_path.extension().ok_or(anyhow::anyhow!( "destination path does not have an extension" ))?; if ext.to_string_lossy().ends_with("tmpl") { let template_dest = template_path .to_str() .and_then(|s| s.strip_suffix(".tmpl")) .ok_or(anyhow::anyhow!("string does not end in .tmpl"))?; return PathBuf::from_str(template_dest).map_err(|e| anyhow::anyhow!(e)); } } Ok(template_path) } } fn get_path_buf_and_check_exists(raw_path: impl Into) -> anyhow::Result { match PathBuf::from_str(&raw_path.into()) { Ok(pb) => { if pb.exists() { Ok(pb) } else { Err(anyhow::anyhow!( "path: {}, could not be found", pb.to_string_lossy() )) } } Err(e) => Err(anyhow::anyhow!(e)), } } fn get_path_buf_and_check_dir_exists(raw_path: impl Into) -> anyhow::Result { match PathBuf::from_str(&raw_path.into()) { Ok(pb) => Ok(pb), Err(e) => Err(anyhow::anyhow!(e)), } }