spawn-cli 0.4.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(&mut tera, &mut context, name)?;

        if path.is_dir() {
            continue;
        }

        tera.add_template_file(path, Some(&name))?;
        collect_vars(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(
    tera: &mut Tera,
    context: &mut Context,
    name: &str,
) -> Result<String, anyhow::Error> {
    tera.add_raw_template(FILENAME_TEMPLATE_NAME, &name)?;
    collect_vars(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: &tera::Template, context: &mut tera::Context) -> Result<()> {
    use tera::ast::{ExprVal, Node};
    let ast = &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 value = inquire::Text::new(&format!("Provide a value for '{ident}':")).prompt()?;

        context.insert(ident, &value);
    }

    Ok(())
}