moon_config 2.0.13

Core workspace, project, and moon configuration.
Documentation
use crate::shapes::OneOrMany;
use crate::{config_enum, config_struct, is_false};
use moon_common::Id;
use rustc_hash::FxHashMap;
use schematic::{Config, ValidateError, validate};
use serde_json::Value;

macro_rules! var_setting {
    ($name:ident, $ty:ty) => {
        config_struct!(
            /// Configuration for a template variable.
            #[derive(Config)]
            pub struct $name {
                /// The default value of the variable if none was provided.
                #[setting(alias = "defaultValue")]
                pub default: $ty,

                /// Marks the variable as internal, and won't be overwritten via CLI arguments.
                #[serde(skip_serializing_if = "is_false")]
                pub internal: bool,

                /// The order in which variables should be prompted for.
                #[serde(skip_serializing_if = "Option::is_none")]
                pub order: Option<usize>,

                /// Prompt the user for a value when the generator is running.
                #[serde(skip_serializing_if = "Option::is_none")]
                pub prompt: Option<String>,

                /// Marks the variable as required, and will not accept an empty value.
                #[serde(skip_serializing_if = "Option::is_none")]
                pub required: Option<bool>,
            }
        );
    };
}

var_setting!(TemplateVariableArraySetting, Vec<Value>);
var_setting!(TemplateVariableBoolSetting, bool);
var_setting!(TemplateVariableNumberSetting, isize);
var_setting!(TemplateVariableObjectSetting, FxHashMap<String, Value>);
var_setting!(TemplateVariableStringSetting, String);

config_struct!(
    #[derive(Config)]
    pub struct TemplateVariableEnumValueConfig {
        /// A human-readable label for the value.
        pub label: String,

        /// The literal enumerable value.
        pub value: String,
    }
);

config_enum!(
    #[derive(Config)]
    #[serde(untagged)]
    pub enum TemplateVariableEnumValue {
        String(String),
        #[setting(nested)]
        Object(TemplateVariableEnumValueConfig),
    }
);

config_enum!(
    #[derive(Config)]
    #[serde(untagged)]
    pub enum TemplateVariableEnumDefault {
        String(String),
        #[setting(default)]
        List(Vec<String>),
    }
);

impl TemplateVariableEnumDefault {
    pub fn to_vec(&self) -> Vec<&String> {
        match self {
            Self::String(value) => vec![value],
            Self::List(list) => list.iter().collect(),
        }
    }
}

fn validate_enum_default<C>(
    default_value: &PartialTemplateVariableEnumDefault,
    partial: &PartialTemplateVariableEnumSetting,
    _context: &C,
    _finalize: bool,
) -> Result<(), ValidateError> {
    if let Some(values) = &partial.values {
        if let PartialTemplateVariableEnumDefault::List(list) = default_value {
            // Vector is the default value, so check if not-empty
            if !partial.multiple.is_some_and(|m| m) && !list.is_empty() {
                return Err(ValidateError::new(
                    "multiple default values is not allowed unless `multiple` is enabled",
                ));
            }
        }

        let values = values
            .iter()
            .flat_map(|v| match v {
                PartialTemplateVariableEnumValue::String(value) => Some(value),
                PartialTemplateVariableEnumValue::Object(cfg) => cfg.value.as_ref(),
            })
            .collect::<Vec<_>>();

        let matches = match default_value {
            PartialTemplateVariableEnumDefault::String(inner) => values.contains(&inner),
            PartialTemplateVariableEnumDefault::List(list) => {
                list.iter().all(|v| values.contains(&v))
            }
        };

        if !matches {
            return Err(ValidateError::new(
                "invalid default value, must be a value configured in `values`",
            ));
        }
    }

    Ok(())
}

config_struct!(
    #[derive(Config)]
    pub struct TemplateVariableEnumSetting {
        /// The default value of the variable if none was provided.
        #[setting(alias = "defaultValue", nested, validate = validate_enum_default)]
        pub default: TemplateVariableEnumDefault,

        /// Marks the variable as internal, and won't be overwritten via CLI arguments.
        #[serde(default, skip_serializing_if = "is_false")]
        pub internal: bool,

        /// Allows multiple values to be selected.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        pub multiple: Option<bool>,

        /// The order in which variables should be prompted for.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        pub order: Option<usize>,

        /// Prompt the user for a value when the generator is running.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        pub prompt: Option<String>,

        /// List of acceptable values for this variable.
        #[setting(nested)]
        pub values: Vec<TemplateVariableEnumValue>,
    }
);

impl TemplateVariableEnumSetting {
    pub fn get_labels(&self) -> Vec<&String> {
        self.values
            .iter()
            .map(|v| match v {
                TemplateVariableEnumValue::String(value) => value,
                TemplateVariableEnumValue::Object(cfg) => &cfg.label,
            })
            .collect()
    }

    pub fn get_values(&self) -> Vec<&String> {
        self.values
            .iter()
            .map(|v| match v {
                TemplateVariableEnumValue::String(value) => value,
                TemplateVariableEnumValue::Object(cfg) => &cfg.value,
            })
            .collect()
    }

    pub fn is_multiple(&self) -> bool {
        self.multiple.is_some_and(|v| v)
    }
}

config_enum!(
    /// Each type of template variable.
    #[derive(Config)]
    #[serde(tag = "type")]
    pub enum TemplateVariable {
        /// An array variable.
        #[setting(nested)]
        Array(TemplateVariableArraySetting),

        /// A boolean variable.
        #[setting(nested)]
        Boolean(TemplateVariableBoolSetting),

        /// A string enumerable variable.
        #[setting(nested)]
        Enum(TemplateVariableEnumSetting),

        /// A number variable.
        #[setting(nested)]
        Number(TemplateVariableNumberSetting),

        /// An object variable.
        #[setting(nested)]
        Object(TemplateVariableObjectSetting),

        /// A string variable.
        #[setting(nested)]
        String(TemplateVariableStringSetting),
    }
);

impl TemplateVariable {
    pub fn get_order(&self) -> usize {
        let order = match self {
            Self::Array(cfg) => cfg.order.as_ref(),
            Self::Boolean(cfg) => cfg.order.as_ref(),
            Self::Enum(cfg) => cfg.order.as_ref(),
            Self::Number(cfg) => cfg.order.as_ref(),
            Self::Object(cfg) => cfg.order.as_ref(),
            Self::String(cfg) => cfg.order.as_ref(),
        };

        order.copied().unwrap_or(100)
    }

    pub fn get_prompt(&self) -> Option<&String> {
        match self {
            Self::Array(cfg) => cfg.prompt.as_ref(),
            Self::Boolean(cfg) => cfg.prompt.as_ref(),
            Self::Enum(cfg) => cfg.prompt.as_ref(),
            Self::Number(cfg) => cfg.prompt.as_ref(),
            Self::Object(cfg) => cfg.prompt.as_ref(),
            Self::String(cfg) => cfg.prompt.as_ref(),
        }
    }

    pub fn is_internal(&self) -> bool {
        match self {
            Self::Array(cfg) => cfg.internal,
            Self::Boolean(cfg) => cfg.internal,
            Self::Enum(cfg) => cfg.internal,
            Self::Number(cfg) => cfg.internal,
            Self::Object(cfg) => cfg.internal,
            Self::String(cfg) => cfg.internal,
        }
    }

    pub fn is_multiple(&self) -> bool {
        match self {
            Self::Enum(cfg) => cfg.is_multiple(),
            _ => false,
        }
    }

    pub fn is_required(&self) -> bool {
        match self {
            Self::Array(cfg) => cfg.required,
            Self::Boolean(cfg) => cfg.required,
            Self::Number(cfg) => cfg.required,
            Self::Object(cfg) => cfg.required,
            Self::String(cfg) => cfg.required,
            _ => None,
        }
        .is_some_and(|v| v)
    }
}

config_struct!(
    /// Configures a template and its files to be scaffolded.
    /// Docs: https://moonrepo.dev/docs/config/template
    #[derive(Config)]
    pub struct TemplateConfig {
        #[setting(rename = "$schema")]
        pub schema: String,

        /// A description on what the template scaffolds.
        #[setting(validate = validate::not_empty)]
        pub description: String,

        /// A pre-populated destination to scaffold to, relative from the
        /// workspace root when leading with `/`, otherwise the working directory.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        pub destination: Option<String>,

        /// Extends one or many other templates.
        #[serde(default, skip_serializing_if = "OneOrMany::is_empty")]
        pub extends: OneOrMany<Id>,

        /// Overrides the identifier of the template, instead of using the folder name.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        pub id: Option<Id>,

        /// A human-readable title for the template.
        #[setting(validate = validate::not_empty)]
        pub title: String,

        /// A map of variables that'll be interpolated within each template file.
        /// Variables can also be populated by passing command line arguments.
        #[setting(nested)]
        #[serde(default, skip_serializing_if = "FxHashMap::is_empty")]
        pub variables: FxHashMap<String, TemplateVariable>,
    }
);