spawn-cli 0.9.0

A command-line tool for creating files and folders from a template.
pub(crate) mod actions;
mod prompt;
mod tera_extensions;

use actions::{Action, ActionVec, Write};
use anyhow::Result;
use log::{info, warn};
use std::path::Path;
use tera::{Context, Tera};

use crate::template::Template;

const FILENAME_TEMPLATE_NAME: &str = "__filename_template";

pub(crate) struct Processor<'a> {
    template: &'a Template<'a>,
}

impl<'a> Processor<'a> {
    pub(crate) fn from_template(template: &'a Template<'a>) -> Self {
        Self { template }
    }

    pub(crate) fn process(&self, cwd: &Path) -> Result<ProcessResult> {
        let plugins = self.template.get_plugins()?;
        let cache_dir = self.template.cache_dir()?;
        let ignore = self.get_ignore()?;
        let mut tera = tera_extensions::extend(Tera::default());
        let mut context = plugins.context(Context::new())?;

        info!("Initial context {context:?}");

        let mut actions: Vec<Action> = Vec::new();

        for path in walkdir::WalkDir::new(&cache_dir)
            .min_depth(1)
            .into_iter()
            .filter_map(Result::ok)
            .filter(|path| {
                pathdiff::diff_paths(path.path(), &cache_dir).is_some_and(|p| !ignore.is_match(&p))
            })
        {
            let path = path.path();
            let Some(rel_path) = pathdiff::diff_paths(path, &cache_dir) else {
                continue;
            };
            let Some(name) = rel_path.to_str() else {
                continue;
            };
            let name = self.process_filename(&mut tera, &mut context, name)?;

            if path.is_dir() {
                continue;
            }

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

            let target = cwd.join(std::path::Path::new(&name));
            let write = Write { name, target };

            actions.push(write.into());
        }

        Ok(ProcessResult {
            tera,
            context,
            actions,
        })
    }

    fn get_ignore(&self) -> Result<globset::GlobSet> {
        use globset::{Glob, GlobSetBuilder};

        let mut builder = GlobSetBuilder::new();

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

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

        Ok(builder.build()?)
    }

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

        Ok(result?)
    }

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

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

            if context.contains_key(identifier) {
                continue;
            }

            let value = prompt::prompt(self.template, identifier)?;

            info!("Collected value {value:?} for {identifier:?}");

            context.insert(identifier, &value);
        }

        Ok(())
    }
}

pub(crate) struct ProcessResult {
    pub(crate) tera: Tera,
    pub(crate) context: Context,
    pub(crate) actions: Vec<Action>,
}

impl ProcessResult {
    pub(crate) fn log(&self) -> std::io::Result<()> {
        self.actions.get_grouped_actions().log()
    }
}