Skip to main content

moon_config/
template_config.rs

1use crate::shapes::OneOrMany;
2use crate::{config_enum, config_struct, is_false};
3use moon_common::Id;
4use rustc_hash::FxHashMap;
5use schematic::{Config, ValidateError, validate};
6use serde_json::Value;
7
8macro_rules! var_setting {
9    ($name:ident, $ty:ty) => {
10        config_struct!(
11            /// Configuration for a template variable.
12            #[derive(Config)]
13            pub struct $name {
14                /// The default value of the variable if none was provided.
15                #[setting(alias = "defaultValue")]
16                pub default: $ty,
17
18                /// Marks the variable as internal, and won't be overwritten via CLI arguments.
19                #[serde(skip_serializing_if = "is_false")]
20                pub internal: bool,
21
22                /// The order in which variables should be prompted for.
23                #[serde(skip_serializing_if = "Option::is_none")]
24                pub order: Option<usize>,
25
26                /// Prompt the user for a value when the generator is running.
27                #[serde(skip_serializing_if = "Option::is_none")]
28                pub prompt: Option<String>,
29
30                /// Marks the variable as required, and will not accept an empty value.
31                #[serde(skip_serializing_if = "Option::is_none")]
32                pub required: Option<bool>,
33            }
34        );
35    };
36}
37
38var_setting!(TemplateVariableArraySetting, Vec<Value>);
39var_setting!(TemplateVariableBoolSetting, bool);
40var_setting!(TemplateVariableNumberSetting, isize);
41var_setting!(TemplateVariableObjectSetting, FxHashMap<String, Value>);
42var_setting!(TemplateVariableStringSetting, String);
43
44config_struct!(
45    #[derive(Config)]
46    pub struct TemplateVariableEnumValueConfig {
47        /// A human-readable label for the value.
48        pub label: String,
49
50        /// The literal enumerable value.
51        pub value: String,
52    }
53);
54
55config_enum!(
56    #[derive(Config)]
57    #[serde(untagged)]
58    pub enum TemplateVariableEnumValue {
59        String(String),
60        #[setting(nested)]
61        Object(TemplateVariableEnumValueConfig),
62    }
63);
64
65config_enum!(
66    #[derive(Config)]
67    #[serde(untagged)]
68    pub enum TemplateVariableEnumDefault {
69        String(String),
70        #[setting(default)]
71        List(Vec<String>),
72    }
73);
74
75impl TemplateVariableEnumDefault {
76    pub fn to_vec(&self) -> Vec<&String> {
77        match self {
78            Self::String(value) => vec![value],
79            Self::List(list) => list.iter().collect(),
80        }
81    }
82}
83
84fn validate_enum_default<C>(
85    default_value: &PartialTemplateVariableEnumDefault,
86    partial: &PartialTemplateVariableEnumSetting,
87    _context: &C,
88    _finalize: bool,
89) -> Result<(), ValidateError> {
90    if let Some(values) = &partial.values {
91        if let PartialTemplateVariableEnumDefault::List(list) = default_value {
92            // Vector is the default value, so check if not-empty
93            if !partial.multiple.is_some_and(|m| m) && !list.is_empty() {
94                return Err(ValidateError::new(
95                    "multiple default values is not allowed unless `multiple` is enabled",
96                ));
97            }
98        }
99
100        let values = values
101            .iter()
102            .flat_map(|v| match v {
103                PartialTemplateVariableEnumValue::String(value) => Some(value),
104                PartialTemplateVariableEnumValue::Object(cfg) => cfg.value.as_ref(),
105            })
106            .collect::<Vec<_>>();
107
108        let matches = match default_value {
109            PartialTemplateVariableEnumDefault::String(inner) => values.contains(&inner),
110            PartialTemplateVariableEnumDefault::List(list) => {
111                list.iter().all(|v| values.contains(&v))
112            }
113        };
114
115        if !matches {
116            return Err(ValidateError::new(
117                "invalid default value, must be a value configured in `values`",
118            ));
119        }
120    }
121
122    Ok(())
123}
124
125config_struct!(
126    #[derive(Config)]
127    pub struct TemplateVariableEnumSetting {
128        /// The default value of the variable if none was provided.
129        #[setting(alias = "defaultValue", nested, validate = validate_enum_default)]
130        pub default: TemplateVariableEnumDefault,
131
132        /// Marks the variable as internal, and won't be overwritten via CLI arguments.
133        #[serde(default, skip_serializing_if = "is_false")]
134        pub internal: bool,
135
136        /// Allows multiple values to be selected.
137        #[serde(default, skip_serializing_if = "Option::is_none")]
138        pub multiple: Option<bool>,
139
140        /// The order in which variables should be prompted for.
141        #[serde(default, skip_serializing_if = "Option::is_none")]
142        pub order: Option<usize>,
143
144        /// Prompt the user for a value when the generator is running.
145        #[serde(default, skip_serializing_if = "Option::is_none")]
146        pub prompt: Option<String>,
147
148        /// List of acceptable values for this variable.
149        #[setting(nested)]
150        pub values: Vec<TemplateVariableEnumValue>,
151    }
152);
153
154impl TemplateVariableEnumSetting {
155    pub fn get_labels(&self) -> Vec<&String> {
156        self.values
157            .iter()
158            .map(|v| match v {
159                TemplateVariableEnumValue::String(value) => value,
160                TemplateVariableEnumValue::Object(cfg) => &cfg.label,
161            })
162            .collect()
163    }
164
165    pub fn get_values(&self) -> Vec<&String> {
166        self.values
167            .iter()
168            .map(|v| match v {
169                TemplateVariableEnumValue::String(value) => value,
170                TemplateVariableEnumValue::Object(cfg) => &cfg.value,
171            })
172            .collect()
173    }
174
175    pub fn is_multiple(&self) -> bool {
176        self.multiple.is_some_and(|v| v)
177    }
178}
179
180config_enum!(
181    /// Each type of template variable.
182    #[derive(Config)]
183    #[serde(tag = "type")]
184    pub enum TemplateVariable {
185        /// An array variable.
186        #[setting(nested)]
187        Array(TemplateVariableArraySetting),
188
189        /// A boolean variable.
190        #[setting(nested)]
191        Boolean(TemplateVariableBoolSetting),
192
193        /// A string enumerable variable.
194        #[setting(nested)]
195        Enum(TemplateVariableEnumSetting),
196
197        /// A number variable.
198        #[setting(nested)]
199        Number(TemplateVariableNumberSetting),
200
201        /// An object variable.
202        #[setting(nested)]
203        Object(TemplateVariableObjectSetting),
204
205        /// A string variable.
206        #[setting(nested)]
207        String(TemplateVariableStringSetting),
208    }
209);
210
211impl TemplateVariable {
212    pub fn get_order(&self) -> usize {
213        let order = match self {
214            Self::Array(cfg) => cfg.order.as_ref(),
215            Self::Boolean(cfg) => cfg.order.as_ref(),
216            Self::Enum(cfg) => cfg.order.as_ref(),
217            Self::Number(cfg) => cfg.order.as_ref(),
218            Self::Object(cfg) => cfg.order.as_ref(),
219            Self::String(cfg) => cfg.order.as_ref(),
220        };
221
222        order.copied().unwrap_or(100)
223    }
224
225    pub fn get_prompt(&self) -> Option<&String> {
226        match self {
227            Self::Array(cfg) => cfg.prompt.as_ref(),
228            Self::Boolean(cfg) => cfg.prompt.as_ref(),
229            Self::Enum(cfg) => cfg.prompt.as_ref(),
230            Self::Number(cfg) => cfg.prompt.as_ref(),
231            Self::Object(cfg) => cfg.prompt.as_ref(),
232            Self::String(cfg) => cfg.prompt.as_ref(),
233        }
234    }
235
236    pub fn is_internal(&self) -> bool {
237        match self {
238            Self::Array(cfg) => cfg.internal,
239            Self::Boolean(cfg) => cfg.internal,
240            Self::Enum(cfg) => cfg.internal,
241            Self::Number(cfg) => cfg.internal,
242            Self::Object(cfg) => cfg.internal,
243            Self::String(cfg) => cfg.internal,
244        }
245    }
246
247    pub fn is_multiple(&self) -> bool {
248        match self {
249            Self::Enum(cfg) => cfg.is_multiple(),
250            _ => false,
251        }
252    }
253
254    pub fn is_required(&self) -> bool {
255        match self {
256            Self::Array(cfg) => cfg.required,
257            Self::Boolean(cfg) => cfg.required,
258            Self::Number(cfg) => cfg.required,
259            Self::Object(cfg) => cfg.required,
260            Self::String(cfg) => cfg.required,
261            _ => None,
262        }
263        .is_some_and(|v| v)
264    }
265}
266
267config_struct!(
268    /// Configures a template and its files to be scaffolded.
269    /// Docs: https://moonrepo.dev/docs/config/template
270    #[derive(Config)]
271    pub struct TemplateConfig {
272        #[setting(rename = "$schema")]
273        pub schema: String,
274
275        /// A description on what the template scaffolds.
276        #[setting(validate = validate::not_empty)]
277        pub description: String,
278
279        /// A pre-populated destination to scaffold to, relative from the
280        /// workspace root when leading with `/`, otherwise the working directory.
281        #[serde(default, skip_serializing_if = "Option::is_none")]
282        pub destination: Option<String>,
283
284        /// Extends one or many other templates.
285        #[serde(default, skip_serializing_if = "OneOrMany::is_empty")]
286        pub extends: OneOrMany<Id>,
287
288        /// Overrides the identifier of the template, instead of using the folder name.
289        #[serde(default, skip_serializing_if = "Option::is_none")]
290        pub id: Option<Id>,
291
292        /// A human-readable title for the template.
293        #[setting(validate = validate::not_empty)]
294        pub title: String,
295
296        /// A map of variables that'll be interpolated within each template file.
297        /// Variables can also be populated by passing command line arguments.
298        #[setting(nested)]
299        #[serde(default, skip_serializing_if = "FxHashMap::is_empty")]
300        pub variables: FxHashMap<String, TemplateVariable>,
301    }
302);