spawn-cli 0.9.0

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

use anyhow::{Error, Result};
use log::info;
use plugins::Plugins;
use std::{path::PathBuf, sync::OnceLock};

use crate::config::cache_dir;
use crate::template::config::Config;

const IGNORE_FILENAME: &str = ".spwnignore";
const CONFIG_DIR: &str = ".spwn";
const CONFIG_FILENAME: &str = "config.toml";
const PLUGINS_FILENAME: &str = "plugins.scm";
const INFO_FILENAME: &str = "info.txt";

pub(crate) struct Template<'a> {
    pub uri: String,
    pub hash: String,
    config: OnceLock<Config<'a>>,
    plugins: OnceLock<Plugins>,
    info: OnceLock<Option<String>>,
}

impl<'a> Template<'a> {
    pub fn new(uri: String) -> Self {
        let hash = create_hash(&uri);

        Template {
            uri,
            hash,
            config: OnceLock::new(),
            plugins: OnceLock::new(),
            info: OnceLock::new(),
        }
    }

    pub fn init(self) -> Result<Self> {
        let cache_dir = self.cache_dir()?;

        if cache_dir.is_dir() {
            return Ok(self);
        }

        crate::repo::clone(&self.uri, &cache_dir)?;

        Ok(self)
    }

    pub fn cache_dir(&self) -> Result<PathBuf> {
        let Some(mut cache_dir) = cache_dir() else {
            return Err(Error::msg("No cache directory"));
        };

        cache_dir.push(&self.hash);

        Ok(cache_dir)
    }

    pub fn config_dir(&self) -> Result<PathBuf> {
        let config_dir = self.cache_dir()?.as_path().join(CONFIG_DIR);

        Ok(config_dir)
    }

    pub fn get_ignore(&self) -> Result<Vec<String>> {
        let template_ignore_file = self.cache_dir()?.as_path().join(IGNORE_FILENAME);

        if !template_ignore_file.is_file() {
            return Err(Error::msg("No template ignore file"));
        }

        info!(
            "Using template ignore file {:?}",
            template_ignore_file.display()
        );

        let lines = crate::fs::read_lines(template_ignore_file)?;

        Ok(lines)
    }

    pub fn get_config(&'a self) -> Result<&'a Config<'a>> {
        if let Some(config) = self.config.get() {
            return Ok(config);
        }

        let config = Config::try_from_template(self)?;
        let _ = self.config.set(config);

        Ok(self.config.get().unwrap())
    }

    pub fn get_plugins(&self) -> Result<&Plugins> {
        if let Some(plugins) = self.plugins.get() {
            return Ok(plugins);
        }

        let plugins_path = self.config_dir()?.as_path().join(PLUGINS_FILENAME);
        let plugins = Plugins::try_from_file(&plugins_path)?;
        let _ = self.plugins.set(plugins);

        Ok(self.plugins.get().unwrap())
    }

    pub fn get_info(&self) -> Result<Option<&String>> {
        if let Some(info) = self.info.get() {
            return Ok(info.as_ref());
        }

        let info_path = self.config_dir()?.as_path().join(INFO_FILENAME);

        if !info_path.is_file() {
            let _ = self.info.set(None);
            return Ok(None);
        }

        info!("Using info from {info_path:?}");

        let info = std::fs::read_to_string(&info_path)?;
        let _ = self.info.set(Some(info));

        Ok(self.info.get().and_then(|i| i.as_ref()))
    }
}

fn create_hash(path: &str) -> String {
    use sha2::{Digest, Sha256};

    let mut hasher = Sha256::new();

    hasher.update(path);

    format!("{:x}", hasher.finalize())
}