Repository: cargo-generate/cargo-generate (rust)

Issue:
should fail if --define placeholder value doesn't match placeholder regex
For reference, the placeholder I am working with is here: https://github.com/waynr/proc-macro-template/blob/main/function-like/cargo-generate.toml

If I add `regex = "^$"` at the bottom of the file and let `cargo-generate` prompt me to enter the value I get:

```
zsh/3 3755 [130]  (git)-[main]-% cargo-generate generate --path function-like --name meow
⚠️   Unable to load config file: /home/wayne/.cargo/cargo-generate.toml
🔧   Generating template ...
🤷   What will the name of your macro be? : funky-bogus-macro
⚠️   Sorry, "funky-bogus-macro" is not a valid value for macro_name
```

On the other hand, using `--define` works fine:

```
zsh/3 3755 [130]  (git)-[main]-% cargo-generate generate --path function-like --name meow -d "macro_name=funky-bogus-macro"
⚠️   Unable to load config file: /home/wayne/.cargo/cargo-generate.toml
macro_name => 'funky-bogus-macro'
🔧   Generating template ...
[ 1/19]   Done: .gitignore
[ 2/19]   Done: Cargo.lock
[ 3/19]   Done: Cargo.toml
[ 4/19]   Done: src/lib.rs
[ 5/19]   Done: src/funky-bogus-macro/analyze.rs
[ 6/19]   Done: src/funky-bogus-macro/codegen.rs
[ 7/19]   Done: src/funky-bogus-macro/lower.rs
[ 8/19]   Done: src/funky-bogus-macro/mod.rs
[ 9/19]   Done: src/funky-bogus-macro/parse.rs
[10/19]   Done: src/funky-bogus-macro
[11/19]   Done: src
[12/19]   Done: tests/funky-bogus-macro/fail/invalid_syntax.rs
[13/19]   Done: tests/funky-bogus-macro/fail/invalid_syntax.stderr
[14/19]   Done: tests/funky-bogus-macro/fail
[15/19]   Done: tests/funky-bogus-macro/pass/valid_syntax.rs
[16/19]   Done: tests/funky-bogus-macro/pass
[17/19]   Done: tests/funky-bogus-macro/ui.rs
[18/19]   Done: tests/funky-bogus-macro
[19/19]   Done: tests
🔧   Moving generated files into: `/home/wayne/projects/waynr/proc-macro-template/meow`...
💡   Initializing a fresh Git repository
✨   Done! New project created /home/wayne/projects/waynr/proc-macro-template/meow
```

My expectation is that this should fail instead of proceeding to use an invalid placeholder value.

Pull request:
should fail if --define placeholder value doesn't match placeholder regex
Fixes #545

Patch:
diff --git a/src/config.rs b/src/config.rs
index f760e8b..183eb4c 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -158,7 +158,7 @@ mod tests {
     }
 
     #[test]
-    fn locate_configs_can_doesnt_look_past_cargo_generate() -> anyhow::Result<()> {
+    fn locate_configs_doesnt_look_past_cargo_generate() -> anyhow::Result<()> {
         let tmp = tempdir().unwrap();
         create_file(&tmp, "dir1/cargo-generate.toml", "")?;
         create_file(&tmp, "dir1/dir2/cargo-generate.toml", "")?;
diff --git a/src/hooks/system_mod.rs b/src/hooks/system_mod.rs
index 5da7232..967f2e1 100644
--- a/src/hooks/system_mod.rs
+++ b/src/hooks/system_mod.rs
@@ -2,7 +2,7 @@ use rhai::{Dynamic, Module};
 use std::process::Command;
 
 use crate::{
-    interactive::prompt_for_variable,
+    interactive::prompt_and_check_variable,
     project_variables::{StringEntry, TemplateSlots, VarInfo},
 };
 
@@ -45,17 +45,20 @@ fn run_command(
             let prompt = format!("The template is requesting to run the following command. Do you agree?\n{full_command}");
 
             // Prompt the user for whether they actually want to run the command.
-            let value = prompt_for_variable(&TemplateSlots {
-                prompt,
-                var_name: "".into(),
-                var_info: VarInfo::String {
-                    entry: Box::new(StringEntry {
-                        default: Some("no".into()),
-                        choices: Some(vec!["yes".into(), "no".into()]),
-                        regex: None,
-                    }),
+            let value = prompt_and_check_variable(
+                &TemplateSlots {
+                    prompt,
+                    var_name: "".into(),
+                    var_info: VarInfo::String {
+                        entry: Box::new(StringEntry {
+                            default: Some("no".into()),
+                            choices: Some(vec!["yes".into(), "no".into()]),
+                            regex: None,
+                        }),
+                    },
                 },
-            });
+                None,
+            );
 
             // Only accept clearly positive affirmations.
             matches!(
diff --git a/src/hooks/variable_mod.rs b/src/hooks/variable_mod.rs
index d5b8265..0ed6329 100644
--- a/src/hooks/variable_mod.rs
+++ b/src/hooks/variable_mod.rs
@@ -5,7 +5,7 @@ use rhai::{Array, Dynamic, Module};
 use std::cell::RefCell;
 use std::rc::Rc;
 
-use crate::interactive::prompt_for_variable;
+use crate::interactive::prompt_and_check_variable;
 use crate::project_variables::{StringEntry, TemplateSlots, VarInfo};
 
 use super::HookResult;
@@ -84,13 +84,16 @@ pub fn create_module(liquid_object: Rc<RefCell<Object>>) -> Module {
 
     module.set_native_fn("prompt", {
         move |prompt: &str, default_value: bool| -> HookResult<bool> {
-            let value = prompt_for_variable(&TemplateSlots {
-                prompt: prompt.into(),
-                var_name: "".into(),
-                var_info: VarInfo::Bool {
-                    default: Some(default_value),
+            let value = prompt_and_check_variable(
+                &TemplateSlots {
+                    prompt: prompt.into(),
+                    var_name: "".into(),
+                    var_info: VarInfo::Bool {
+                        default: Some(default_value),
+                    },
                 },
-            });
+                None,
+            );
 
             match value {
                 Ok(v) => Ok(v.parse::<bool>().map_err(|_| "Unable to parse into bool")?),
@@ -101,17 +104,20 @@ pub fn create_module(liquid_object: Rc<RefCell<Object>>) -> Module {
 
     module.set_native_fn("prompt", {
         move |prompt: &str| -> HookResult<String> {
-            let value = prompt_for_variable(&TemplateSlots {
-                prompt: prompt.into(),
-                var_name: "".into(),
-                var_info: VarInfo::String {
-                    entry: Box::new(StringEntry {
-                        default: None,
-                        choices: None,
-                        regex: None,
-                    }),
+            let value = prompt_and_check_variable(
+                &TemplateSlots {
+                    prompt: prompt.into(),
+                    var_name: "".into(),
+                    var_info: VarInfo::String {
+                        entry: Box::new(StringEntry {
+                            default: None,
+                            choices: None,
+                            regex: None,
+                        }),
+                    },
                 },
-            });
+                None,
+            );
 
             match value {
                 Ok(v) => Ok(v),
@@ -122,17 +128,20 @@ pub fn create_module(liquid_object: Rc<RefCell<Object>>) -> Module {
 
     module.set_native_fn("prompt", {
         move |prompt: &str, default_value: &str| -> HookResult<String> {
-            let value = prompt_for_variable(&TemplateSlots {
-                prompt: prompt.into(),
-                var_name: "".into(),
-                var_info: VarInfo::String {
-                    entry: Box::new(StringEntry {
-                        default: Some(default_value.into()),
-                        choices: None,
-                        regex: None,
-                    }),
+            let value = prompt_and_check_variable(
+                &TemplateSlots {
+                    prompt: prompt.into(),
+                    var_name: "".into(),
+                    var_info: VarInfo::String {
+                        entry: Box::new(StringEntry {
+                            default: Some(default_value.into()),
+                            choices: None,
+                            regex: None,
+                        }),
+                    },
                 },
-            });
+                None,
+            );
 
             match value {
                 Ok(v) => Ok(v),
@@ -143,17 +152,20 @@ pub fn create_module(liquid_object: Rc<RefCell<Object>>) -> Module {
 
     module.set_native_fn("prompt", {
         move |prompt: &str, default_value: &str, regex: &str| -> HookResult<String> {
-            let value = prompt_for_variable(&TemplateSlots {
-                prompt: prompt.into(),
-                var_name: "".into(),
-                var_info: VarInfo::String {
-                    entry: Box::new(StringEntry {
-                        default: Some(default_value.into()),
-                        choices: None,
-                        regex: Some(Regex::new(regex).map_err(|_| "Invalid regex")?),
-                    }),
+            let value = prompt_and_check_variable(
+                &TemplateSlots {
+                    prompt: prompt.into(),
+                    var_name: "".into(),
+                    var_info: VarInfo::String {
+                        entry: Box::new(StringEntry {
+                            default: Some(default_value.into()),
+                            choices: None,
+                            regex: Some(Regex::new(regex).map_err(|_| "Invalid regex")?),
+                        }),
+                    },
                 },
-            });
+                None,
+            );
 
             match value {
                 Ok(v) => Ok(v),
@@ -164,22 +176,25 @@ pub fn create_module(liquid_object: Rc<RefCell<Object>>) -> Module {
 
     module.set_native_fn("prompt", {
         move |prompt: &str, default_value: &str, choices: rhai::Array| -> HookResult<String> {
-            let value = prompt_for_variable(&TemplateSlots {
-                prompt: prompt.into(),
-                var_name: "".into(),
-                var_info: VarInfo::String {
-                    entry: Box::new(StringEntry {
-                        default: Some(default_value.into()),
-                        choices: Some(
-                            choices
-                                .iter()
-                                .map(|d| d.to_owned().into_string().unwrap())
-                                .collect(),
-                        ),
-                        regex: None,
-                    }),
+            let value = prompt_and_check_variable(
+                &TemplateSlots {
+                    prompt: prompt.into(),
+                    var_name: "".into(),
+                    var_info: VarInfo::String {
+                        entry: Box::new(StringEntry {
+                            default: Some(default_value.into()),
+                            choices: Some(
+                                choices
+                                    .iter()
+                                    .map(|d| d.to_owned().into_string().unwrap())
+                                    .collect(),
+                            ),
+                            regex: None,
+                        }),
+                    },
                 },
-            });
+                None,
+            );
 
             match value {
                 Ok(v) => Ok(v),
diff --git a/src/interactive.rs b/src/interactive.rs
index 541d10b..49f1ca1 100644
--- a/src/interactive.rs
+++ b/src/interactive.rs
@@ -2,12 +2,12 @@ use crate::{
     emoji,
     project_variables::{StringEntry, TemplateSlots, VarInfo},
 };
-use anyhow::Result;
+use anyhow::{bail, Result};
 use console::style;
-use dialoguer::theme::ColorfulTheme;
 use dialoguer::Input;
+use dialoguer::{theme::ColorfulTheme, Select};
 use liquid_core::Value;
-use std::ops::Index;
+use std::{ops::Index, str::FromStr};
 
 pub fn name() -> Result<String> {
     let valid_ident = regex::Regex::new(r"^([a-zA-Z][a-zA-Z0-9_-]+)$")?;
@@ -22,7 +22,7 @@ pub fn name() -> Result<String> {
             }),
         },
     };
-    prompt_for_variable(&project_var)
+    prompt_and_check_variable(&project_var, None)
 }
 
 pub fn user_question(prompt: &str, default: &Option<String>) -> Result<String> {
@@ -34,83 +34,152 @@ pub fn user_question(prompt: &str, default: &Option<String>) -> Result<String> {
     i.interact().map_err(Into::<anyhow::Error>::into)
 }
 
-pub fn prompt_for_variable(variable: &TemplateSlots) -> Result<String> {
-    use dialoguer::Select;
-
+pub fn prompt_and_check_variable(
+    variable: &TemplateSlots,
+    provided_value: Option<String>,
+) -> Result<String> {
     let prompt = format!("{} {}", emoji::SHRUG, style(&variable.prompt).bold(),);
 
     match &variable.var_info {
-        VarInfo::Bool { default } => {
-            let choices = [false.to_string(), true.to_string()];
-            let chosen = Select::with_theme(&ColorfulTheme::default())
-                .items(&choices)
-                .with_prompt(&prompt)
-                .default(if default.unwrap_or(false) { 1 } else { 0 })
-                .interact()?;
-
-            Ok(choices.index(chosen).to_string())
-        }
+        VarInfo::Bool { default } => handle_bool_input(provided_value, &prompt, default),
         VarInfo::String { entry } => match &entry.choices {
             Some(choices) => {
-                let default = entry
-                    .default
-                    .as_ref()
-                    .map_or(0, |default| choices.binary_search(default).unwrap_or(0));
-                let chosen = Select::with_theme(&ColorfulTheme::default())
-                    .items(choices)
-                    .with_prompt(&prompt)
-                    .default(default)
-                    .interact()?;
-
-                Ok(choices.index(chosen).to_string())
-            }
-            None => {
-                let prompt = format!(
-                    "{} {}",
-                    prompt,
-                    match &entry.default {
-                        Some(d) => format!("[default: {}]", style(d).bold()),
-                        None => "".into(),
-                    }
-                );
-                let default = entry.default.as_ref().map(|v| v.into());
-
-                match &entry.regex {
-                    Some(regex) => loop {
-                        let user_entry = user_question(prompt.as_str(), &default)?;
-                        if regex.is_match(&user_entry) {
-                            break Ok(user_entry);
-                        }
-                        eprintln!(
-                            "{} {} \"{}\" {}",
-                            emoji::WARN,
-                            style("Sorry,").bold().red(),
-                            style(&user_entry).bold().yellow(),
-                            style(format!("is not a valid value for {}", variable.var_name))
-                                .bold()
-                                .red()
-                        );
-                    },
-                    None => Ok(user_question(prompt.as_str(), &default)?),
-                }
+                handle_choice_input(provided_value, &variable.var_name, choices, entry, &prompt)
             }
+            None => handle_string_input(provided_value, &variable.var_name, entry, &prompt),
         },
     }
 }
 
 pub fn variable(variable: &TemplateSlots, provided_value: Option<&impl ToString>) -> Result<Value> {
-    let user_input = provided_value
-        .map(|v| Ok(v.to_string()))
-        .unwrap_or_else(|| prompt_for_variable(variable))?;
-    into_value(user_input, &variable.var_info)
-}
-
-fn into_value(user_entry: String, var_info: &VarInfo) -> Result<Value> {
-    match var_info {
+    let user_entry = prompt_and_check_variable(variable, provided_value.map(|v| v.to_string()))?;
+    match &variable.var_info {
         VarInfo::Bool { .. } => {
-            let as_bool = user_entry.parse::<bool>()?; // this shouldn't fail if checked before
+            let as_bool = user_entry.parse::<bool>()?;
             Ok(Value::Scalar(as_bool.into()))
         }
         VarInfo::String { .. } => Ok(Value::Scalar(user_entry.into())),
     }
 }
+
+fn handle_string_input(
+    provided_value: Option<String>,
+    var_name: &str,
+    entry: &StringEntry,
+    prompt: &str,
+) -> Result<String> {
+    match provided_value {
+        Some(value) => {
+            if entry
+                .regex
+                .as_ref()
+                .map(|ex| ex.is_match(&value))
+                .unwrap_or(true)
+            {
+                Ok(value)
+            } else {
+                bail!(
+                    "{} {} \"{}\" {}",
+                    emoji::WARN,
+                    style("Sorry,").bold().red(),
+                    style(&value).bold().yellow(),
+                    style(format!("is not a valid value for {}", var_name))
+                        .bold()
+                        .red()
+                )
+            }
+        }
+        None => {
+            let prompt = format!(
+                "{} {}",
+                prompt,
+                match &entry.default {
+                    Some(d) => format!("[default: {}]", style(d).bold()),
+                    None => "".into(),
+                }
+            );
+            let default = entry.default.as_ref().map(|v| v.into());
+
+            match &entry.regex {
+                Some(regex) => loop {
+                    let user_entry = user_question(prompt.as_str(), &default)?;
+                    if regex.is_match(&user_entry) {
+                        break Ok(user_entry);
+                    }
+                    eprintln!(
+                        "{} {} \"{}\" {}",
+                        emoji::WARN,
+                        style("Sorry,").bold().red(),
+                        style(&user_entry).bold().yellow(),
+                        style(format!("is not a valid value for {}", var_name))
+                            .bold()
+                            .red()
+                    );
+                },
+                None => Ok(user_question(prompt.as_str(), &default)?),
+            }
+        }
+    }
+}
+
+fn handle_choice_input(
+    provided_value: Option<String>,
+    var_name: &str,
+    choices: &Vec<String>,
+    entry: &StringEntry,
+    prompt: &str,
+) -> Result<String> {
+    match provided_value {
+        Some(value) => {
+            if choices.contains(&value) {
+                Ok(value)
+            } else {
+                bail!(
+                    "{} {} \"{}\" {}",
+                    emoji::WARN,
+                    style("Sorry,").bold().red(),
+                    style(&value).bold().yellow(),
+                    style(format!("is not a valid value for {}", var_name))
+                        .bold()
+                        .red(),
+                )
+            }
+        }
+        None => {
+            let default = entry
+                .default
+                .as_ref()
+                .map_or(0, |default| choices.binary_search(default).unwrap_or(0));
+            let chosen = Select::with_theme(&ColorfulTheme::default())
+                .items(choices)
+                .with_prompt(prompt)
+                .default(default)
+                .interact()?;
+
+            Ok(choices.index(chosen).to_string())
+        }
+    }
+}
+
+fn handle_bool_input(
+    provided_value: Option<String>,
+    prompt: &str,
+    default: &Option<bool>,
+) -> Result<String> {
+    match provided_value {
+        Some(value) => {
+            let value = bool::from_str(&value.to_lowercase())?;
+            Ok(value.to_string())
+        }
+        None => {
+            let choices = [false.to_string(), true.to_string()];
+            let chosen = Select::with_theme(&ColorfulTheme::default())
+                .items(&choices)
+                .with_prompt(prompt)
+                .default(if default.unwrap_or(false) { 1 } else { 0 })
+                .interact()?;
+
+            Ok(choices.index(chosen).to_string())
+        }
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index a8db9a7..08a928a 100755
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -51,7 +51,7 @@ use console::style;
 use git::DEFAULT_BRANCH;
 use hooks::execute_hooks;
 use ignore_me::remove_dir_files;
-use interactive::prompt_for_variable;
+use interactive::prompt_and_check_variable;
 use project_variables::{StringEntry, TemplateSlots, VarInfo};
 use std::ffi::OsString;
 use std::{
@@ -202,7 +202,9 @@ fn get_source_template_into_temp(
             branch = branch2;
         }
         TemplateLocation::Path(path) => {
-            temp_dir = copy_path_template_into_temp(path)?;
+            temp_dir = tempfile::tempdir()?;
+            copy_dir_all(path, temp_dir.path(), false)?;
+            git::remove_history(temp_dir.path())?;
             branch = String::from(DEFAULT_BRANCH); // FIXME is here any reason to set branch when path is used?
         }
     };
@@ -210,16 +212,33 @@ fn get_source_template_into_temp(
     Ok((temp_dir, branch))
 }
 
+fn resolve_project_name(args: &GenerateArgs) -> Result<ProjectName> {
+    match args.name {
+        Some(ref n) => Ok(ProjectName::new(n)),
+        None if !args.silent => Ok(ProjectName::new(interactive::name()?)),
+        None => Err(anyhow!(
+            "{} {} {}",
+            emoji::ERROR,
+            style("Project Name Error:").bold().red(),
+            style("Option `--silent` provided, but project name was not set. Please use `--name`.")
+                .bold()
+                .red(),
+        )),
+    }
+}
+
 /// resolve the template location for the actual template to expand
 fn resolve_template_dir(template_base_dir: &TempDir, subfolder: Option<&str>) -> Result<PathBuf> {
     let template_dir = resolve_template_dir_subfolder(template_base_dir.path(), subfolder)?;
-    auto_locate_template_dir(template_dir, &mut prompt_for_variable)
+    auto_locate_template_dir(template_dir, &mut |slots| {
+        prompt_and_check_variable(slots, None)
+    })
 }
 
 /// join the base-dir and the sufolder, ensuring that we stay within the template directory
-fn resolve_template_dir_subfolder<P: AsRef<str>>(
+fn resolve_template_dir_subfolder(
     template_base_dir: &Path,
-    subfolder: Option<P>,
+    subfolder: Option<impl AsRef<str>>,
 ) -> Result<PathBuf> {
     if let Some(subfolder) = subfolder {
         let template_base_dir = fs::canonicalize(template_base_dir)?;
@@ -256,7 +275,7 @@ fn resolve_template_dir_subfolder<P: AsRef<str>>(
 
         Ok(template_dir)
     } else {
-        Ok(template_base_dir.to_path_buf())
+        Ok(template_base_dir.to_owned())
     }
 }
 
@@ -336,29 +355,6 @@ fn resolve_configured_sub_templates(
         .unwrap_or_else(|| Ok(config_path.to_path_buf()))
 }
 
-fn resolve_project_name(args: &GenerateArgs) -> Result<ProjectName> {
-    match args.name {
-        Some(ref n) => Ok(ProjectName::new(n)),
-        None if !args.silent => Ok(ProjectName::new(interactive::name()?)),
-        None => Err(anyhow!(
-            "{} {} {}",
-            emoji::ERROR,
-            style("Project Name Error:").bold().red(),
-            style("Option `--silent` provided, but project name was not set. Please use `--name`.")
-                .bold()
-                .red(),
-        )),
-    }
-}
-
-fn copy_path_template_into_temp(src_path: &Path) -> Result<TempDir> {
-    let path_clone_dir = tempfile::tempdir()?;
-    copy_dir_all(src_path, path_clone_dir.path(), false)?;
-    git::remove_history(path_clone_dir.path())?;
-
-    Ok(path_clone_dir)
-}
-
 pub(crate) fn copy_dir_all(
     src: impl AsRef<Path>,
     dst: impl AsRef<Path>,
@@ -594,11 +590,7 @@ fn fill_placeholders_and_merge_conditionals(
     template_values: &HashMap<String, toml::Value>,
     args: &GenerateArgs,
 ) -> Result<liquid::Object, anyhow::Error> {
-    let conditionals = config.conditional.take();
-    if conditionals.is_none() {
-        return Ok(liquid_object);
-    }
-    let mut conditionals = conditionals.unwrap();
+    let mut conditionals = config.conditional.take().unwrap_or_default();
 
     loop {
         project_variables::fill_project_variables(&mut liquid_object, config, |slot| {


Test patch:
diff --git a/tests/integration/config_file/values.rs b/tests/integration/config_file/values.rs
index 0ca000a..efe9519 100644
--- a/tests/integration/config_file/values.rs
+++ b/tests/integration/config_file/values.rs
@@ -375,3 +375,42 @@ fn cli_value_overrides_others() {
     let cargo_toml = dir.read("foobar-project/Cargo.toml");
     assert!(cargo_toml.contains("content of cli-value"));
 }
+
+#[test]
+fn cli_values_are_checked_via_regex() {
+    let template = tmp_dir()
+        .file(
+            "Cargo.toml",
+            indoc! {r#"
+                [package]
+                name = "{{project-name}}"
+                description = "{{my_value}}"
+                version = "0.1.0"
+            "#},
+        )
+        .file(
+            "cargo-generate.toml",
+            indoc! {r#"
+                [placeholders.my_value]
+                type = "string"
+                prompt = "What will the name of 'my_value' be?"
+                regex = "^$"
+            "#},
+        )
+        .init_git()
+        .build();
+
+    let dir = tmp_dir().build();
+
+    binary()
+        .arg("generate")
+        .arg("--name")
+        .arg("foobar-project")
+        .arg("-d")
+        .arg(r#"my_value="content of my-value""#)
+        .arg("--path")
+        .arg(template.path())
+        .current_dir(&dir.path())
+        .assert()
+        .failure();
+}