diff --git a/crates/templates/src/constraints.rs b/crates/templates/src/constraints.rs index 538b621317..a19f779fc6 100644 --- a/crates/templates/src/constraints.rs +++ b/crates/templates/src/constraints.rs @@ -3,6 +3,7 @@ use regex::Regex; #[derive(Clone, Debug)] pub(crate) struct StringConstraints { pub regex: Option, + pub allowed_values: Option>, } impl StringConstraints { @@ -12,6 +13,15 @@ impl StringConstraints { anyhow::bail!("Input '{}' does not match pattern '{}'", text, regex); } } + if let Some(allowed_values) = self.allowed_values.as_ref() { + if !allowed_values.contains(&text) { + anyhow::bail!( + "Input '{}' is not one of the allowed values ({})", + text, + allowed_values.join(", ") + ); + } + } Ok(text) } } diff --git a/crates/templates/src/interaction.rs b/crates/templates/src/interaction.rs index 50025ec386..3b989c8fa5 100644 --- a/crates/templates/src/interaction.rs +++ b/crates/templates/src/interaction.rs @@ -8,7 +8,7 @@ use crate::{ use anyhow::anyhow; // use console::style; -use dialoguer::{Confirm, Input}; +use dialoguer::{Confirm, Input, Select}; pub(crate) trait InteractionStrategy { fn allow_generate_into(&self, target_dir: &Path) -> Cancellable<(), anyhow::Error>; @@ -111,7 +111,10 @@ pub(crate) fn prompt_parameter(parameter: &TemplateParameter) -> Option loop { let input = match parameter.data_type() { - TemplateParameterDataType::String(_) => ask_free_text(prompt, default_value), + TemplateParameterDataType::String(constraints) => match &constraints.allowed_values { + Some(allowed_values) => ask_choice(prompt, default_value, allowed_values), + None => ask_free_text(prompt, default_value), + }, }; match input { @@ -138,6 +141,21 @@ fn ask_free_text(prompt: &str, default_value: &Option) -> anyhow::Result Ok(result) } +fn ask_choice( + prompt: &str, + default_value: &Option, + allowed_values: &[String], +) -> anyhow::Result { + let mut select = Select::new().with_prompt(prompt).items(allowed_values); + if let Some(s) = default_value { + if let Some(default_index) = allowed_values.iter().position(|item| item == s) { + select = select.default(default_index); + } + } + let selected_index = select.interact()?; + Ok(allowed_values[selected_index].clone()) +} + fn is_directory_empty(path: &Path) -> bool { if !path.exists() { return true; diff --git a/crates/templates/src/reader.rs b/crates/templates/src/reader.rs index 70bf5622a6..40b05a2a55 100644 --- a/crates/templates/src/reader.rs +++ b/crates/templates/src/reader.rs @@ -86,6 +86,7 @@ pub(crate) struct RawParameter { #[serde(rename = "default")] pub default_value: Option, pub pattern: Option, + pub allowed_values: Option>, } #[derive(Debug, Deserialize)] diff --git a/crates/templates/src/template.rs b/crates/templates/src/template.rs index 02fca2525b..2b34082a41 100644 --- a/crates/templates/src/template.rs +++ b/crates/templates/src/template.rs @@ -617,7 +617,10 @@ impl Condition { fn parse_string_constraints(raw: &RawParameter) -> anyhow::Result { let regex = raw.pattern.as_ref().map(|re| Regex::new(re)).transpose()?; - Ok(StringConstraints { regex }) + Ok(StringConstraints { + regex, + allowed_values: raw.allowed_values.clone(), + }) } fn read_install_record(layout: &TemplateLayout) -> InstalledFrom {