spawn-cli 0.5.0

A command-line tool for creating files and folders from a template.
use anyhow::Result;
use globset::{Glob, GlobSet, GlobSetBuilder};
use log::{info, warn};
use std::{env, fs::File};
use tera::{Context, Tera};

use crate::{
    config::{get_global_ignore, Config},
    template::Template,
};

const FILENAME_TEMPLATE_NAME: &str = "__filename_template";

pub(crate) fn spawn(config: &Config, uri: String) -> Result<()> {
    let uri = config.resolve_alias(uri);

    info!("Using template {uri:?}");

    let cwd = env::current_dir()?;

    info!("The current directory is {:?}", cwd.display());

    let template = Template::from_uri(uri);
    let cache_dir = template.init()?.cache_dir()?;
    let ignore = get_ignore(&template);
    let mut tera = Tera::default();
    let mut context = Context::new();

    for path in walkdir::WalkDir::new(&cache_dir)
        .min_depth(1)
        .into_iter()
        .filter_map(Result::ok)
        .filter(|path| {
            let path = pathdiff::diff_paths(path.path(), &cache_dir).unwrap();

            !ignore.is_match(&path)
        })
    {
        let path = path.path();
        let name = pathdiff::diff_paths(path, &cache_dir).unwrap();
        let name = name.to_str().unwrap();
        let name = process_filename(&template, &mut tera, &mut context, name)?;

        if path.is_dir() {
            continue;
        }

        tera.add_template_file(path, Some(&name))?;
        collect_vars(&template, tera.get_template(&name)?, &mut context)?;

        let mut target = cwd.clone();
        target.push(std::path::Path::new(&name));

        info!("Writing to {target:?}");

        if let Some(parent) = target.parent() {
            std::fs::create_dir_all(parent)?;
        }

        let file = File::create(target)?;

        tera.render_to(&name, &context, file)?;
    }

    Ok(())
}

fn get_ignore(template: &Template) -> GlobSet {
    let mut builder = GlobSetBuilder::new();

    match template.get_ignore() {
        Ok(lines) => {
            for line in lines {
                builder.add(Glob::new(&line).unwrap());
            }
        }
        Err(e) => warn!("Not using template ignore: {:?}", e),
    };

    match get_global_ignore() {
        Ok(lines) => {
            for line in lines {
                builder.add(Glob::new(&line).unwrap());
            }
        }
        Err(e) => warn!("Not using global ignore: {:?}", e),
    };

    let ignore = builder.build().unwrap();

    ignore
}

fn process_filename(
    template: &Template,
    tera: &mut Tera,
    context: &mut Context,
    name: &str,
) -> Result<String, anyhow::Error> {
    tera.add_raw_template(FILENAME_TEMPLATE_NAME, &name)?;
    collect_vars(
        template,
        tera.get_template(FILENAME_TEMPLATE_NAME)?,
        context,
    )?;
    let result = tera.render(FILENAME_TEMPLATE_NAME, &*context);
    tera.templates.remove(FILENAME_TEMPLATE_NAME);
    let name = result?;
    Ok(name)
}

fn collect_vars(
    template: &Template,
    tera_template: &tera::Template,
    context: &mut tera::Context,
) -> Result<()> {
    let template_config = template.get_config();
    use tera::ast::{ExprVal, Node};
    let ast = &tera_template.ast;

    for node in ast {
        let Node::VariableBlock(_, expr) = node else {
            continue;
        };
        let ExprVal::Ident(ident) = &expr.val else {
            continue;
        };

        if context.contains_key(ident) {
            continue;
        }

        let message = format!("Provide a value for '{ident}':");
        let (message, help_message) = match template_config.get_var(&ident) {
            Some(var) => {
                let message = match &var.message {
                    Some(message) => message,
                    None => &message,
                };
                let help_message = var.help_message.as_ref();

                (message, help_message)
            }
            None => (&message, None),
        };

        let prompt = inquire::Text::new(message);
        let prompt = match help_message {
            Some(help_message) => prompt.with_help_message(help_message),
            None => prompt,
        };
        let value = prompt.prompt()?;

        context.insert(ident, &value);
    }

    Ok(())
}