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::{
    Serialize,
    ser::{SerializeMap, SerializeSeq},
};
use std::cell::RefCell;
use std::path::Path;
use steel::{SteelErr, SteelVal, steel_vm::engine::Engine};
use tera::Context;

const FUNCTION_CWD: &str = "cwd";
const FUNCTION_INFO: &str = "info";
const FUNCTION_CONTEXT: &str = "context";
const FUNCTION_MESSAGE: &str = "message";
const FUNCTION_HELP_MESSAGE: &str = "help-message ";
const FUNCTION_PLACEHOLDER: &str = "placeholder ";
const FUNCTION_INITIAL_VALUE: &str = "initial-value ";
const FUNCTION_DEFAULT: &str = "default ";
const FUNCTION_VALIDATE: &str = "validate";
const FUNCTION_OPTIONS: &str = "options";

#[derive(Clone)]
pub(crate) struct Plugins {
    vm: RefCell<Option<Engine>>,
}

impl Plugins {
    pub(super) fn try_from_file(path: &Path) -> Result<Self> {
        if !path.is_file() {
            let vm = RefCell::new(None);

            return Ok(Self { vm });
        }

        info!("Using plugins file {path:?}");

        let plugins_data = std::fs::read_to_string(path)?;

        plugins_data.try_into()
    }

    fn call_function(
        &self,
        name: &str,
        arguments: Vec<SteelVal>,
    ) -> Option<Result<SteelVal, SteelErr>> {
        let mut vm = self.vm.borrow_mut();
        let vm = vm.as_mut()?;

        if !vm.global_exists(name) {
            return None;
        }

        let result = vm.call_function_by_name_with_args(name, arguments);

        Some(result)
    }

    fn map_string_with_function(
        &self,
        name: &str,
        arguments: &[&str],
        default: Option<&str>,
    ) -> Result<String> {
        let default = default.or_else(|| arguments.first().copied()).unwrap_or("");
        let arguments = arguments
            .iter()
            .map(|a| a.to_string().into())
            .collect::<Vec<SteelVal>>();
        let Some(result) = self.call_function(name, arguments) else {
            return Ok(default.to_string());
        };
        let SteelVal::StringV(result) = result? else {
            return Err(anyhow::Error::msg(format!(
                "Plugin {name:?} should return a string"
            )));
        };
        let result = result.to_string();

        Ok(result)
    }

    pub(crate) fn cwd(&self, cwd: &str) -> Result<String> {
        self.map_string_with_function(FUNCTION_CWD, &[cwd], None)
    }

    pub(crate) fn info(&self, info: Option<&str>) -> Result<Option<String>> {
        let info = info.unwrap_or("");
        let info = self.map_string_with_function(FUNCTION_INFO, &[info], None)?;
        let info = if info.is_empty() { None } else { Some(info) };

        Ok(info)
    }

    pub(crate) fn context(&self, context: Context) -> Result<Context> {
        let arguments = vec![];
        let Some(result) = self.call_function(FUNCTION_CONTEXT, arguments) else {
            return Ok(context);
        };
        let SteelVal::HashMapV(context) = result? else {
            return Err(anyhow::Error::msg(format!(
                "Plugin {FUNCTION_CONTEXT:?} should return a hashmap"
            )));
        };
        let context = context
            .iter()
            .fold(Context::new(), |mut context, (key, val)| {
                let key: SerializableSteelVal = key.into();
                let val: SerializableSteelVal = val.into();

                context.insert(key, &val);

                context
            });

        Ok(context)
    }

    pub(crate) fn message(&self, identifier: &str, message: &str) -> Result<String> {
        self.map_string_with_function(FUNCTION_MESSAGE, &[identifier, message], Some(message))
    }

    pub(crate) fn help_message(
        &self,
        identifier: &str,
        help_message: Option<&str>,
    ) -> Result<Option<String>> {
        let help_message = help_message.unwrap_or("");
        let help_message = self.map_string_with_function(
            FUNCTION_HELP_MESSAGE,
            &[identifier, help_message],
            Some(help_message),
        )?;
        let help_message = if help_message.is_empty() {
            None
        } else {
            Some(help_message)
        };

        Ok(help_message)
    }

    pub(crate) fn placeholder(
        &self,
        identifier: &str,
        placeholder: Option<&str>,
    ) -> Result<Option<String>> {
        let placeholder = placeholder.unwrap_or("");
        let placeholder = self.map_string_with_function(
            FUNCTION_PLACEHOLDER,
            &[identifier, placeholder],
            Some(placeholder),
        )?;
        let placeholder = if placeholder.is_empty() {
            None
        } else {
            Some(placeholder)
        };

        Ok(placeholder)
    }

    pub(crate) fn initial_value(
        &self,
        identifier: &str,
        initial_value: Option<&str>,
    ) -> Result<Option<String>> {
        let initial_value = initial_value.unwrap_or("");
        let initial_value = self.map_string_with_function(
            FUNCTION_INITIAL_VALUE,
            &[identifier, initial_value],
            Some(initial_value),
        )?;
        let initial_value = if initial_value.is_empty() {
            None
        } else {
            Some(initial_value)
        };

        Ok(initial_value)
    }

    pub(crate) fn default(
        &self,
        identifier: &str,
        default: Option<&str>,
    ) -> Result<Option<String>> {
        let default = default.unwrap_or("");
        let default =
            self.map_string_with_function(FUNCTION_DEFAULT, &[identifier, default], Some(default))?;
        let default = if default.is_empty() {
            None
        } else {
            Some(default)
        };

        Ok(default)
    }

    pub(crate) fn validate(&self, identifier: &str, value: &str) -> Result<Result<(), String>> {
        let arguments = vec![identifier.to_string().into(), value.to_string().into()];
        let Some(result) = self.call_function(FUNCTION_VALIDATE, arguments) else {
            return Ok(Ok(()));
        };

        let result = match result? {
            SteelVal::BoolV(bool) => {
                if bool {
                    Ok(())
                } else {
                    Err("Invalid value".into())
                }
            }
            SteelVal::StringV(steel_string) => Err(steel_string.to_string()),
            _ => Ok(()),
        };

        Ok(result)
    }

    pub(crate) fn options(&self, identifier: &str, value: &[String]) -> Result<Vec<String>> {
        let value_argument = value
            .iter()
            .map(|v| SteelVal::StringV(v.into()))
            .collect::<Vec<SteelVal>>();
        let value_argument = SteelVal::ListV(value_argument.into());
        let arguments = vec![identifier.to_string().into(), value_argument];
        let Some(result) = self.call_function(FUNCTION_OPTIONS, arguments) else {
            return Ok(value.to_vec());
        };
        let SteelVal::ListV(result) = result? else {
            return Err(anyhow::Error::msg(format!(
                "Plugin {FUNCTION_OPTIONS:?} should return a list"
            )));
        };
        let result = result.into_iter().try_fold(Vec::new(), |mut result, v| {
            let v = match v {
                SteelVal::StringV(steel_string) => steel_string.to_string(),
                SteelVal::NumV(int) => int.to_string(),
                SteelVal::IntV(int) => int.to_string(),
                SteelVal::CharV(char) => char.to_string(),
                _ => {
                    return Err(anyhow::Error::msg(
                        "List returned by {FUNCTION_OPTIONS:?} should only contain string values",
                    ));
                }
            };

            result.push(v);

            Ok(result)
        })?;

        Ok(result)
    }
}

impl TryFrom<String> for Plugins {
    type Error = anyhow::Error;

    fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
        let mut vm = steel::steel_vm::engine::Engine::new();

        vm.run(value)?;

        let vm = RefCell::new(Some(vm));

        Ok(Self { vm })
    }
}

struct SerializableSteelVal<'a> {
    steel_val: &'a SteelVal,
}

impl<'a> From<&'a SteelVal> for SerializableSteelVal<'a> {
    fn from(value: &'a SteelVal) -> Self {
        Self { steel_val: value }
    }
}

impl Serialize for SerializableSteelVal<'_> {
    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        match self.steel_val {
            SteelVal::BoolV(bool) => serializer.serialize_bool(*bool),
            SteelVal::NumV(num) => serializer.serialize_f64(*num),
            SteelVal::IntV(int) => serializer.serialize_i64(*int as i64),
            SteelVal::CharV(char) => serializer.serialize_char(*char),
            SteelVal::StringV(steel_string) | SteelVal::SymbolV(steel_string) => {
                serializer.serialize_str(&steel_string.to_string())
            }
            SteelVal::VectorV(steel_vector) => {
                let items: Vec<_> = steel_vector.iter().collect();
                let mut seq = serializer.serialize_seq(Some(items.len()))?;
                for element in &items {
                    let element: SerializableSteelVal = (*element).into();
                    seq.serialize_element(&element)?;
                }
                seq.end()
            }
            SteelVal::HashMapV(steel_hash_map) => {
                let entries: Vec<_> = steel_hash_map.iter().collect();
                let mut map = serializer.serialize_map(Some(entries.len()))?;
                for (key, value) in &entries {
                    let key: SerializableSteelVal = (*key).into();
                    let value: SerializableSteelVal = (*value).into();
                    map.serialize_entry(&key, &value)?;
                }
                map.end()
            }
            SteelVal::ListV(generic_list) => {
                let mut seq = serializer.serialize_seq(Some(generic_list.len()))?;
                for value in generic_list {
                    let value: SerializableSteelVal = value.into();
                    seq.serialize_element(&value)?;
                }
                seq.end()
            }
            _ => Err(serde::ser::Error::custom(format!(
                "unsupported SteelVal variant for serialization: {}",
                self.steel_val
            ))),
        }
    }
}

impl From<SerializableSteelVal<'_>> for String {
    fn from(value: SerializableSteelVal) -> Self {
        match value.steel_val {
            SteelVal::StringV(steel_string) => steel_string.to_string(),
            _ => value.steel_val.to_string(),
        }
    }
}