spawn-cli 0.9.0

A command-line tool for creating files and folders from a template.
use anyhow::Result;
use log::info;
use serde::Deserialize;
use std::path::Path;

use super::{CONFIG_FILENAME, Template};

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

impl<'a> Config<'a> {
    pub(super) fn try_from_template(template: &'a Template<'a>) -> Result<Config<'a>> {
        let config_path = template.config_dir()?.as_path().join(CONFIG_FILENAME);

        if !config_path.is_file() {
            return Ok(Self {
                template,
                config_file: ConfigFile::default(),
            });
        }

        info!("Using template config file {config_path:?}");

        let config_file = ConfigFile::try_from_file(&config_path)?;

        Ok(Config {
            template,
            config_file,
        })
    }

    pub(crate) fn get_var(&self, identifier: &str) -> Result<Var> {
        let var = self.config_file.get_var(identifier);
        let plugins = self.template.get_plugins()?;
        let default_message = format!("Provide a value for '{identifier}':");

        let var = match var {
            Var::Text {
                identifier: _,
                message,
                help_message,
                placeholder,
                initial_value,
                default,
            } => {
                let message = match message {
                    Some(message) => message,
                    None => default_message,
                };
                let message = plugins.message(identifier, &message)?;
                let help_message = plugins.help_message(identifier, help_message.as_deref())?;
                let placeholder = plugins.placeholder(identifier, placeholder.as_deref())?;
                let initial_value = plugins.initial_value(identifier, initial_value.as_deref())?;
                let default = plugins.default(identifier, default.as_deref())?;

                Var::Text {
                    identifier: identifier.to_string(),
                    message: Some(message),
                    help_message,
                    placeholder,
                    initial_value,
                    default,
                }
            }
            Var::Select {
                identifier: _,
                message,
                options,
                help_message,
            } => {
                let message = match message {
                    Some(message) => message,
                    None => default_message,
                };
                let message = plugins.message(identifier, &message)?;
                let options = plugins.options(identifier, &options)?;
                let help_message = plugins.help_message(identifier, help_message.as_deref())?;

                Var::Select {
                    identifier: identifier.to_string(),
                    message: Some(message),
                    options,
                    help_message,
                }
            }
        };

        Ok(var)
    }
}

#[derive(Default, Deserialize)]
struct ConfigFile {
    #[serde(rename = "var", default = "Vec::new")]
    vars: Vec<Var>,
}

impl ConfigFile {
    fn try_from_file(path: &Path) -> Result<ConfigFile> {
        let config_data = std::fs::read_to_string(path)?;
        let config: ConfigFile = toml::from_str(&config_data)?;

        Ok(config)
    }

    fn get_var(&self, identifier: &str) -> Var {
        self.vars
            .iter()
            .find(|v| Var::has_identifier(v, identifier))
            .cloned()
            .unwrap_or_default()
    }
}

#[derive(Clone, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub(crate) enum Var {
    Text {
        identifier: String,
        message: Option<String>,
        help_message: Option<String>,
        placeholder: Option<String>,
        initial_value: Option<String>,
        default: Option<String>,
    },
    Select {
        identifier: String,
        message: Option<String>,
        options: Vec<String>,
        help_message: Option<String>,
    },
}

impl Var {
    fn has_identifier(&self, name: &str) -> bool {
        match self {
            Var::Text { identifier, .. } | Var::Select { identifier, .. } => identifier == name,
        }
    }
}

impl Default for Var {
    fn default() -> Self {
        Var::Text {
            identifier: String::new(),
            message: None,
            help_message: None,
            placeholder: None,
            initial_value: None,
            default: None,
        }
    }
}