cargo_generate/
lib.rs

1#![doc = include_str!("../README.md")]
2#![warn(
3    //clippy::cargo_common_metadata,
4    clippy::branches_sharing_code,
5    clippy::cast_lossless,
6    clippy::cognitive_complexity,
7    clippy::get_unwrap,
8    clippy::if_then_some_else_none,
9    clippy::inefficient_to_string,
10    clippy::match_bool,
11    clippy::missing_const_for_fn,
12    clippy::missing_panics_doc,
13    clippy::option_if_let_else,
14    clippy::redundant_closure,
15    clippy::redundant_else,
16    clippy::redundant_pub_crate,
17    clippy::ref_binding_to_reference,
18    clippy::ref_option_ref,
19    clippy::same_functions_in_if_condition,
20    clippy::unneeded_field_pattern,
21    clippy::unnested_or_patterns,
22    clippy::use_self,
23)]
24
25mod app_config;
26mod args;
27mod config;
28mod copy;
29mod emoji;
30mod favorites;
31mod filenames;
32mod git;
33mod hooks;
34mod ignore_me;
35mod include_exclude;
36mod interactive;
37mod progressbar;
38mod project_variables;
39mod template;
40mod template_filters;
41mod template_variables;
42mod user_parsed_input;
43mod workspace_member;
44
45pub use crate::app_config::{app_config_path, AppConfig};
46pub use crate::favorites::list_favorites;
47use crate::template::create_liquid_engine;
48pub use args::*;
49
50use anyhow::{anyhow, bail, Context, Result};
51use config::{locate_template_configs, Config, CONFIG_FILE_NAME};
52use console::style;
53use copy::copy_files_recursively;
54use env_logger::fmt::Formatter;
55use fs_err as fs;
56use hooks::execute_hooks;
57use ignore_me::remove_dir_files;
58use interactive::prompt_and_check_variable;
59use log::Record;
60use log::{info, warn};
61use project_variables::{StringEntry, StringKind, TemplateSlots, VarInfo};
62use std::{
63    cell::RefCell,
64    collections::HashMap,
65    env,
66    io::Write,
67    path::{Path, PathBuf},
68    sync::{Arc, Mutex},
69};
70use tempfile::TempDir;
71use user_parsed_input::{TemplateLocation, UserParsedInput};
72use workspace_member::WorkspaceMemberStatus;
73
74use crate::git::tmp_dir;
75use crate::template_variables::{
76    load_env_and_args_template_values, CrateName, ProjectDir, ProjectNameInput,
77};
78use crate::{project_variables::ConversionError, template_variables::ProjectName};
79
80use self::config::TemplateConfig;
81use self::git::try_get_branch_from_path;
82use self::hooks::evaluate_script;
83use self::template::{create_liquid_object, set_project_name_variables, LiquidObjectResource};
84
85/// Logging formatter function
86pub fn log_formatter(
87    buf: &mut Formatter,
88    record: &Record,
89) -> std::result::Result<(), std::io::Error> {
90    let prefix = match record.level() {
91        log::Level::Error => format!("{} ", emoji::ERROR),
92        log::Level::Warn => format!("{} ", emoji::WARN),
93        _ => "".to_string(),
94    };
95
96    writeln!(buf, "{}{}", prefix, record.args())
97}
98
99/// # Panics
100pub fn generate(args: GenerateArgs) -> Result<PathBuf> {
101    let _working_dir_scope = ScopedWorkingDirectory::default();
102
103    let app_config = AppConfig::try_from(app_config_path(&args.config)?.as_path())?;
104
105    // mash AppConfig and CLI arguments together into UserParsedInput
106    let mut user_parsed_input = UserParsedInput::try_from_args_and_config(app_config, &args);
107    // let ENV vars provide values we don't have yet
108    user_parsed_input
109        .template_values_mut()
110        .extend(load_env_and_args_template_values(&args)?);
111
112    let (template_base_dir, template_dir, branch) = prepare_local_template(&user_parsed_input)?;
113
114    // read configuration in the template
115    let mut config = Config::from_path(
116        &locate_template_file(CONFIG_FILE_NAME, &template_base_dir, &template_dir).ok(),
117    )?;
118
119    // the `--init` parameter may also be set by the template itself
120    if config
121        .template
122        .as_ref()
123        .and_then(|c| c.init)
124        .unwrap_or(false)
125        && !user_parsed_input.init
126    {
127        warn!(
128            "{}",
129            style("Template specifies --init, while not specified on the command line. Output location is affected!").bold().red(),
130        );
131
132        user_parsed_input.init = true;
133    };
134
135    check_cargo_generate_version(&config)?;
136
137    let project_dir = expand_template(&template_dir, &mut config, &user_parsed_input, &args)?;
138    let (mut should_initialize_git, with_force) = {
139        let vcs = &config
140            .template
141            .as_ref()
142            .and_then(|t| t.vcs)
143            .unwrap_or_else(|| user_parsed_input.vcs());
144
145        (
146            !vcs.is_none() && (!user_parsed_input.init || user_parsed_input.force_git_init()),
147            user_parsed_input.force_git_init(),
148        )
149    };
150
151    let target_path = if user_parsed_input.test() {
152        test_expanded_template(&template_dir, args.other_args)?
153    } else {
154        let project_path = copy_expanded_template(template_dir, project_dir, user_parsed_input)?;
155
156        match workspace_member::add_to_workspace(&project_path)? {
157            WorkspaceMemberStatus::Added(workspace_cargo_toml) => {
158                should_initialize_git = with_force;
159                info!(
160                    "{} {} `{}`",
161                    emoji::WRENCH,
162                    style("Project added as member to workspace").bold(),
163                    style(workspace_cargo_toml.display()).bold().yellow(),
164                );
165            }
166            WorkspaceMemberStatus::NoWorkspaceFound => {
167                // not an issue, just a notification
168            }
169        }
170
171        project_path
172    };
173
174    if should_initialize_git {
175        info!(
176            "{} {}",
177            emoji::WRENCH,
178            style("Initializing a fresh Git repository").bold()
179        );
180
181        git::init(&target_path, branch.as_deref(), with_force)?;
182    }
183
184    info!(
185        "{} {} {} {}",
186        emoji::SPARKLE,
187        style("Done!").bold().green(),
188        style("New project created").bold(),
189        style(&target_path.display()).underlined()
190    );
191
192    Ok(target_path)
193}
194
195fn copy_expanded_template(
196    template_dir: PathBuf,
197    project_dir: PathBuf,
198    user_parsed_input: UserParsedInput,
199) -> Result<PathBuf> {
200    info!(
201        "{} {} `{}`{}",
202        emoji::WRENCH,
203        style("Moving generated files into:").bold(),
204        style(project_dir.display()).bold().yellow(),
205        style("...").bold()
206    );
207    copy_files_recursively(template_dir, &project_dir, user_parsed_input.overwrite())?;
208
209    Ok(project_dir)
210}
211
212fn test_expanded_template(template_dir: &PathBuf, args: Option<Vec<String>>) -> Result<PathBuf> {
213    info!(
214        "{} {}{}{}",
215        emoji::WRENCH,
216        style("Running \"").bold(),
217        style("cargo test"),
218        style("\" ...").bold(),
219    );
220    std::env::set_current_dir(template_dir)?;
221    let (cmd, cmd_args) = std::env::var("CARGO_GENERATE_TEST_CMD").map_or_else(
222        |_| (String::from("cargo"), vec![String::from("test")]),
223        |env_test_cmd| {
224            let mut split_cmd_args = env_test_cmd.split_whitespace().map(str::to_string);
225            (
226                split_cmd_args.next().unwrap(),
227                split_cmd_args.collect::<Vec<String>>(),
228            )
229        },
230    );
231    std::process::Command::new(cmd)
232        .args(cmd_args)
233        .args(args.unwrap_or_default().into_iter())
234        .spawn()?
235        .wait()?
236        .success()
237        .then(PathBuf::new)
238        .ok_or_else(|| anyhow!("{} Testing failed", emoji::ERROR))
239}
240
241fn prepare_local_template(
242    source_template: &UserParsedInput,
243) -> Result<(TempDir, PathBuf, Option<String>), anyhow::Error> {
244    let (temp_dir, branch) = get_source_template_into_temp(source_template.location())?;
245    let template_folder = resolve_template_dir(&temp_dir, source_template.subfolder())?;
246
247    Ok((temp_dir, template_folder, branch))
248}
249
250fn get_source_template_into_temp(
251    template_location: &TemplateLocation,
252) -> Result<(TempDir, Option<String>)> {
253    match template_location {
254        TemplateLocation::Git(git) => {
255            let result = git::clone_git_template_into_temp(
256                git.url(),
257                git.branch(),
258                git.tag(),
259                git.revision(),
260                git.identity(),
261                git.gitconfig(),
262                git.skip_submodules,
263            );
264            if let Ok((ref temp_dir, _)) = result {
265                git::remove_history(temp_dir.path())?;
266            };
267            result
268        }
269        TemplateLocation::Path(path) => {
270            let temp_dir = tmp_dir()?;
271            copy_files_recursively(path, temp_dir.path(), false)?;
272            git::remove_history(temp_dir.path())?;
273            Ok((temp_dir, try_get_branch_from_path(path)))
274        }
275    }
276}
277
278/// resolve the template location for the actual template to expand
279fn resolve_template_dir(template_base_dir: &TempDir, subfolder: Option<&str>) -> Result<PathBuf> {
280    let template_dir = resolve_template_dir_subfolder(template_base_dir.path(), subfolder)?;
281    auto_locate_template_dir(template_dir, &mut |slots| {
282        prompt_and_check_variable(slots, None)
283    })
284}
285
286/// join the base-dir and the subfolder, ensuring that we stay within the template directory
287fn resolve_template_dir_subfolder(
288    template_base_dir: &Path,
289    subfolder: Option<impl AsRef<str>>,
290) -> Result<PathBuf> {
291    if let Some(subfolder) = subfolder {
292        let template_base_dir = fs::canonicalize(template_base_dir)?;
293        let template_dir = fs::canonicalize(template_base_dir.join(subfolder.as_ref()))
294            .with_context(|| {
295                format!(
296                    "not able to find subfolder '{}' in source template",
297                    subfolder.as_ref()
298                )
299            })?;
300
301        // make sure subfolder is not `../../subfolder`
302        if !template_dir.starts_with(&template_base_dir) {
303            return Err(anyhow!(
304                "{} {} {}",
305                emoji::ERROR,
306                style("Subfolder Error:").bold().red(),
307                style("Invalid subfolder. Must be part of the template folder structure.")
308                    .bold()
309                    .red(),
310            ));
311        }
312
313        if !template_dir.is_dir() {
314            return Err(anyhow!(
315                "{} {} {}",
316                emoji::ERROR,
317                style("Subfolder Error:").bold().red(),
318                style("The specified subfolder must be a valid folder.")
319                    .bold()
320                    .red(),
321            ));
322        }
323
324        Ok(template_dir)
325    } else {
326        Ok(template_base_dir.to_owned())
327    }
328}
329
330/// look through the template folder structure and attempt to find a suitable template.
331fn auto_locate_template_dir(
332    template_base_dir: PathBuf,
333    prompt: &mut impl FnMut(&TemplateSlots) -> Result<String>,
334) -> Result<PathBuf> {
335    let config_paths = locate_template_configs(&template_base_dir)?;
336    match config_paths.len() {
337        0 => {
338            // No configurations found, so this *must* be a template
339            Ok(template_base_dir)
340        }
341        1 => {
342            // A single configuration found, but it may contain multiple configured sub-templates
343            resolve_configured_sub_templates(&template_base_dir.join(&config_paths[0]), prompt)
344        }
345        _ => {
346            // Multiple configurations found, each in different "roots"
347            // let user select between them
348            let prompt_args = TemplateSlots {
349                prompt: "Which template should be expanded?".into(),
350                var_name: "Template".into(),
351                var_info: VarInfo::String {
352                    entry: Box::new(StringEntry {
353                        default: Some(config_paths[0].display().to_string()),
354                        kind: StringKind::Choices(
355                            config_paths
356                                .into_iter()
357                                .map(|p| p.display().to_string())
358                                .collect(),
359                        ),
360                        regex: None,
361                    }),
362                },
363            };
364            let path = prompt(&prompt_args)?;
365
366            // recursively retry to resolve the template,
367            // until we hit a single or no config, idetifying the final template folder
368            auto_locate_template_dir(template_base_dir.join(path), prompt)
369        }
370    }
371}
372
373fn resolve_configured_sub_templates(
374    config_path: &Path,
375    prompt: &mut impl FnMut(&TemplateSlots) -> Result<String>,
376) -> Result<PathBuf> {
377    Config::from_path(&Some(config_path.join(CONFIG_FILE_NAME)))
378        .ok()
379        .and_then(|config| config.template)
380        .and_then(|config| config.sub_templates)
381        .map_or_else(
382            || Ok(PathBuf::from(config_path)),
383            |sub_templates| {
384                // we have a config that defines sub-templates, let the user select
385                let prompt_args = TemplateSlots {
386                    prompt: "Which sub-template should be expanded?".into(),
387                    var_name: "Template".into(),
388                    var_info: VarInfo::String {
389                        entry: Box::new(StringEntry {
390                            default: Some(sub_templates[0].clone()),
391                            kind: StringKind::Choices(sub_templates.clone()),
392                            regex: None,
393                        }),
394                    },
395                };
396                let path = prompt(&prompt_args)?;
397
398                // recursively retry to resolve the template,
399                // until we hit a single or no config, idetifying the final template folder
400                auto_locate_template_dir(
401                    resolve_template_dir_subfolder(config_path, Some(path))?,
402                    prompt,
403                )
404            },
405        )
406}
407
408fn locate_template_file(
409    name: &str,
410    template_base_folder: impl AsRef<Path>,
411    template_folder: impl AsRef<Path>,
412) -> Result<PathBuf> {
413    let template_base_folder = template_base_folder.as_ref();
414    let mut search_folder = template_folder.as_ref().to_path_buf();
415    loop {
416        let file_path = search_folder.join::<&str>(name);
417        if file_path.exists() {
418            return Ok(file_path);
419        }
420        if search_folder == template_base_folder {
421            bail!("File not found within template");
422        }
423        search_folder = search_folder
424            .parent()
425            .ok_or_else(|| anyhow!("Reached root folder"))?
426            .to_path_buf();
427    }
428}
429
430fn expand_template(
431    template_dir: &Path,
432    config: &mut Config,
433    user_parsed_input: &UserParsedInput,
434    args: &GenerateArgs,
435) -> Result<PathBuf> {
436    let liquid_object = create_liquid_object(user_parsed_input)?;
437
438    // run init hooks - these won't have access to `crate_name`/`within_cargo_project`
439    // variables, as these are not set yet. Furthermore, if `project-name` is set, it is the raw
440    // user input!
441    // The init hooks are free to set `project-name` (but it will be validated before further
442    // use).
443    execute_hooks(
444        template_dir,
445        &liquid_object,
446        &config.get_init_hooks(),
447        user_parsed_input.allow_commands(),
448        user_parsed_input.silent(),
449    )?;
450
451    let project_name_input = ProjectNameInput::try_from((&liquid_object, user_parsed_input))?;
452    let project_name = ProjectName::from((&project_name_input, user_parsed_input));
453    let crate_name = CrateName::from(&project_name_input);
454    let destination = ProjectDir::try_from((&project_name_input, user_parsed_input))?;
455    if !user_parsed_input.init() {
456        destination.create()?;
457    }
458
459    set_project_name_variables(&liquid_object, &destination, &project_name, &crate_name)?;
460
461    info!(
462        "{} {} {}",
463        emoji::WRENCH,
464        style(format!("Destination: {destination}")).bold(),
465        style("...").bold()
466    );
467    info!(
468        "{} {} {}",
469        emoji::WRENCH,
470        style(format!("project-name: {project_name}")).bold(),
471        style("...").bold()
472    );
473    project_variables::show_project_variables_with_value(&liquid_object, config);
474
475    info!(
476        "{} {} {}",
477        emoji::WRENCH,
478        style("Generating template").bold(),
479        style("...").bold()
480    );
481
482    // evaluate config for placeholders and and any that are undefined
483    fill_placeholders_and_merge_conditionals(
484        config,
485        &liquid_object,
486        user_parsed_input.template_values(),
487        args,
488    )?;
489    add_missing_provided_values(&liquid_object, user_parsed_input.template_values())?;
490
491    // run pre-hooks
492    execute_hooks(
493        template_dir,
494        &liquid_object,
495        &config.get_pre_hooks(),
496        user_parsed_input.allow_commands(),
497        user_parsed_input.silent(),
498    )?;
499
500    // walk/evaluate the template
501    let all_hook_files = config.get_hook_files();
502    let mut template_config = config.template.take().unwrap_or_default();
503
504    ignore_me::remove_unneeded_files(template_dir, &template_config.ignore, args.verbose)?;
505    let mut pbar = progressbar::new();
506
507    let rhai_filter_files = Arc::new(Mutex::new(vec![]));
508    let rhai_engine = create_liquid_engine(
509        template_dir.to_owned(),
510        liquid_object.clone(),
511        user_parsed_input.allow_commands(),
512        user_parsed_input.silent(),
513        rhai_filter_files.clone(),
514    );
515    template::walk_dir(
516        &mut template_config,
517        template_dir,
518        &all_hook_files,
519        &liquid_object,
520        rhai_engine,
521        &rhai_filter_files,
522        &mut pbar,
523        args.verbose,
524    )?;
525
526    // run post-hooks
527    execute_hooks(
528        template_dir,
529        &liquid_object,
530        &config.get_post_hooks(),
531        user_parsed_input.allow_commands(),
532        user_parsed_input.silent(),
533    )?;
534
535    // remove all hook and filter files as they are never part of the template output
536    let rhai_filter_files = rhai_filter_files
537        .lock()
538        .unwrap()
539        .iter()
540        .cloned()
541        .collect::<Vec<_>>();
542    remove_dir_files(
543        all_hook_files
544            .into_iter()
545            .map(PathBuf::from)
546            .chain(rhai_filter_files),
547        false,
548    );
549
550    config.template.replace(template_config);
551    Ok(destination.as_ref().to_owned())
552}
553
554/// Try to add all provided `template_values` to the `liquid_object`.
555///
556/// ## Note:
557/// Values for which a placeholder exists, should already be filled by `fill_project_variables`
558pub(crate) fn add_missing_provided_values(
559    liquid_object: &LiquidObjectResource,
560    template_values: &HashMap<String, toml::Value>,
561) -> Result<(), anyhow::Error> {
562    template_values.iter().try_for_each(|(k, v)| {
563        if RefCell::borrow(&liquid_object.lock().unwrap()).contains_key(k.as_str()) {
564            return Ok(());
565        }
566        // we have a value without a slot in the liquid object.
567        // try to create the slot from the provided value
568        let value = match v {
569            toml::Value::String(content) => liquid_core::Value::Scalar(content.clone().into()),
570            toml::Value::Boolean(content) => liquid_core::Value::Scalar((*content).into()),
571            _ => anyhow::bail!(format!(
572                "{} {}",
573                emoji::ERROR,
574                style("Unsupported value type. Only Strings and Booleans are supported.")
575                    .bold()
576                    .red(),
577            )),
578        };
579        liquid_object
580            .lock()
581            .unwrap()
582            .borrow_mut()
583            .insert(k.clone().into(), value);
584        Ok(())
585    })?;
586    Ok(())
587}
588
589fn read_default_variable_value_from_template(slot: &TemplateSlots) -> Result<String, ()> {
590    let default_value = match &slot.var_info {
591        VarInfo::Bool {
592            default: Some(default),
593        } => default.to_string(),
594        VarInfo::String {
595            entry: string_entry,
596        } => match *string_entry.clone() {
597            StringEntry {
598                default: Some(default),
599                ..
600            } => default.clone(),
601            _ => return Err(()),
602        },
603        _ => return Err(()),
604    };
605    let (key, value) = (&slot.var_name, &default_value);
606    info!(
607        "{} {} (default value from template)",
608        emoji::WRENCH,
609        style(format!("{key}: {value:?}")).bold(),
610    );
611    Ok(default_value)
612}
613
614// Evaluate the configuration, adding defined placeholder variables to the liquid object.
615fn fill_placeholders_and_merge_conditionals(
616    config: &mut Config,
617    liquid_object: &LiquidObjectResource,
618    template_values: &HashMap<String, toml::Value>,
619    args: &GenerateArgs,
620) -> Result<()> {
621    let mut conditionals = config.conditional.take().unwrap_or_default();
622
623    loop {
624        // keep evaluating for placeholder variables as long new ones are added.
625        project_variables::fill_project_variables(liquid_object, config, |slot| {
626            let provided_value = template_values.get(&slot.var_name).and_then(|v| match v {
627                toml::Value::String(s) => Some(s.clone()),
628                toml::Value::Integer(s) => Some(s.to_string()),
629                toml::Value::Float(s) => Some(s.to_string()),
630                toml::Value::Boolean(s) => Some(s.to_string()),
631                toml::Value::Datetime(s) => Some(s.to_string()),
632                toml::Value::Array(_) | toml::Value::Table(_) => None,
633            });
634            if provided_value.is_none() && args.silent {
635                let default_value = match read_default_variable_value_from_template(slot) {
636                    Ok(string) => string,
637                    Err(()) => {
638                        anyhow::bail!(ConversionError::MissingDefaultValueForPlaceholderVariable {
639                            var_name: slot.var_name.clone()
640                        })
641                    }
642                };
643                interactive::variable(slot, Some(&default_value))
644            } else {
645                interactive::variable(slot, provided_value.as_ref())
646            }
647        })?;
648
649        let placeholders_changed = conditionals
650            .iter_mut()
651            // filter each conditional config block by trueness of the expression, given the known variables
652            .filter_map(|(key, cfg)| {
653                evaluate_script::<bool>(liquid_object, key)
654                    .ok()
655                    .filter(|&r| r)
656                    .map(|_| cfg)
657            })
658            .map(|conditional_template_cfg| {
659                // append the conditional blocks configuration, returning true if any placeholders were added
660                let template_cfg = config.template.get_or_insert_with(TemplateConfig::default);
661                if let Some(mut extras) = conditional_template_cfg.include.take() {
662                    template_cfg
663                        .include
664                        .get_or_insert_with(Vec::default)
665                        .append(&mut extras);
666                }
667                if let Some(mut extras) = conditional_template_cfg.exclude.take() {
668                    template_cfg
669                        .exclude
670                        .get_or_insert_with(Vec::default)
671                        .append(&mut extras);
672                }
673                if let Some(mut extras) = conditional_template_cfg.ignore.take() {
674                    template_cfg
675                        .ignore
676                        .get_or_insert_with(Vec::default)
677                        .append(&mut extras);
678                }
679                if let Some(extra_placeholders) = conditional_template_cfg.placeholders.take() {
680                    match config.placeholders.as_mut() {
681                        Some(placeholders) => {
682                            for (k, v) in extra_placeholders.0 {
683                                placeholders.0.insert(k, v);
684                            }
685                        }
686                        None => {
687                            config.placeholders = Some(extra_placeholders);
688                        }
689                    };
690                    return true;
691                }
692                false
693            })
694            .fold(false, |acc, placeholders_changed| {
695                acc | placeholders_changed
696            });
697
698        if !placeholders_changed {
699            break;
700        }
701    }
702
703    Ok(())
704}
705
706fn check_cargo_generate_version(template_config: &Config) -> Result<(), anyhow::Error> {
707    if let Config {
708        template:
709            Some(config::TemplateConfig {
710                cargo_generate_version: Some(requirement),
711                ..
712            }),
713        ..
714    } = template_config
715    {
716        let version = semver::Version::parse(env!("CARGO_PKG_VERSION"))?;
717        if !requirement.matches(&version) {
718            bail!(
719                "{} {} {} {} {}",
720                emoji::ERROR,
721                style("Required cargo-generate version not met. Required:")
722                    .bold()
723                    .red(),
724                style(requirement).yellow(),
725                style(" was:").bold().red(),
726                style(version).yellow(),
727            );
728        }
729    }
730    Ok(())
731}
732
733#[derive(Debug)]
734struct ScopedWorkingDirectory(PathBuf);
735
736impl Default for ScopedWorkingDirectory {
737    fn default() -> Self {
738        Self(env::current_dir().unwrap())
739    }
740}
741
742impl Drop for ScopedWorkingDirectory {
743    fn drop(&mut self) {
744        env::set_current_dir(&self.0).unwrap();
745    }
746}
747
748#[cfg(test)]
749mod tests {
750    use crate::{
751        auto_locate_template_dir,
752        project_variables::{StringKind, VarInfo},
753        tmp_dir,
754    };
755    use anyhow::anyhow;
756    use std::{
757        fs,
758        io::Write,
759        path::{Path, PathBuf},
760    };
761    use tempfile::TempDir;
762
763    #[test]
764    fn auto_locate_template_returns_base_when_no_cargo_generate_is_found() -> anyhow::Result<()> {
765        let tmp = tmp_dir().unwrap();
766        create_file(&tmp, "dir1/Cargo.toml", "")?;
767        create_file(&tmp, "dir2/dir2_1/Cargo.toml", "")?;
768        create_file(&tmp, "dir3/Cargo.toml", "")?;
769
770        let actual =
771            auto_locate_template_dir(tmp.path().to_path_buf(), &mut |_slots| Err(anyhow!("test")))?
772                .canonicalize()?;
773        let expected = tmp.path().canonicalize()?;
774
775        assert_eq!(expected, actual);
776        Ok(())
777    }
778
779    #[test]
780    fn auto_locate_template_returns_path_when_single_cargo_generate_is_found() -> anyhow::Result<()>
781    {
782        let tmp = tmp_dir().unwrap();
783        create_file(&tmp, "dir1/Cargo.toml", "")?;
784        create_file(&tmp, "dir2/dir2_1/Cargo.toml", "")?;
785        create_file(&tmp, "dir2/dir2_2/cargo-generate.toml", "")?;
786        create_file(&tmp, "dir3/Cargo.toml", "")?;
787
788        let actual =
789            auto_locate_template_dir(tmp.path().to_path_buf(), &mut |_slots| Err(anyhow!("test")))?
790                .canonicalize()?;
791        let expected = tmp.path().join("dir2/dir2_2").canonicalize()?;
792
793        assert_eq!(expected, actual);
794        Ok(())
795    }
796
797    #[test]
798    fn auto_locate_template_can_resolve_configured_subtemplates() -> anyhow::Result<()> {
799        let tmp = tmp_dir().unwrap();
800        create_file(
801            &tmp,
802            "cargo-generate.toml",
803            indoc::indoc! {r#"
804                [template]
805                sub_templates = ["sub1", "sub2"]
806            "#},
807        )?;
808        create_file(&tmp, "sub1/Cargo.toml", "")?;
809        create_file(&tmp, "sub2/Cargo.toml", "")?;
810
811        let actual = auto_locate_template_dir(tmp.path().to_path_buf(), &mut |slots| match &slots
812            .var_info
813        {
814            VarInfo::Bool { .. } => anyhow::bail!("Wrong prompt type"),
815            VarInfo::String { entry } => {
816                if let StringKind::Choices(choices) = entry.kind.clone() {
817                    let expected = vec!["sub1".to_string(), "sub2".to_string()];
818                    assert_eq!(expected, choices);
819                    Ok("sub2".to_string())
820                } else {
821                    anyhow::bail!("Missing choices")
822                }
823            }
824        })?
825        .canonicalize()?;
826        let expected = tmp.path().join("sub2").canonicalize()?;
827
828        assert_eq!(expected, actual);
829        Ok(())
830    }
831
832    #[test]
833    fn auto_locate_template_recurses_to_resolve_subtemplates() -> anyhow::Result<()> {
834        let tmp = tmp_dir().unwrap();
835        create_file(
836            &tmp,
837            "cargo-generate.toml",
838            indoc::indoc! {r#"
839                [template]
840                sub_templates = ["sub1", "sub2"]
841            "#},
842        )?;
843        create_file(&tmp, "sub1/Cargo.toml", "")?;
844        create_file(&tmp, "sub1/sub11/cargo-generate.toml", "")?;
845        create_file(
846            &tmp,
847            "sub1/sub12/cargo-generate.toml",
848            indoc::indoc! {r#"
849                [template]
850                sub_templates = ["sub122", "sub121"]
851            "#},
852        )?;
853        create_file(&tmp, "sub2/Cargo.toml", "")?;
854        create_file(&tmp, "sub1/sub11/Cargo.toml", "")?;
855        create_file(&tmp, "sub1/sub12/sub121/Cargo.toml", "")?;
856        create_file(&tmp, "sub1/sub12/sub122/Cargo.toml", "")?;
857
858        let mut prompt_num = 0;
859        let actual = auto_locate_template_dir(tmp.path().to_path_buf(), &mut |slots| match &slots
860            .var_info
861        {
862            VarInfo::Bool { .. } => anyhow::bail!("Wrong prompt type"),
863            VarInfo::String { entry } => {
864                if let StringKind::Choices(choices) = entry.kind.clone() {
865                    let (expected, answer) = match prompt_num {
866                        0 => (vec!["sub1", "sub2"], "sub1"),
867                        1 => (vec!["sub11", "sub12"], "sub12"),
868                        2 => (vec!["sub122", "sub121"], "sub121"),
869                        _ => panic!("Unexpected number of prompts"),
870                    };
871                    prompt_num += 1;
872                    expected
873                        .into_iter()
874                        .zip(choices.iter())
875                        .for_each(|(a, b)| assert_eq!(a, b));
876                    Ok(answer.to_string())
877                } else {
878                    anyhow::bail!("Missing choices")
879                }
880            }
881        })?
882        .canonicalize()?;
883
884        let expected = tmp
885            .path()
886            .join("sub1")
887            .join("sub12")
888            .join("sub121")
889            .canonicalize()?;
890
891        assert_eq!(expected, actual);
892        Ok(())
893    }
894
895    #[test]
896    fn auto_locate_template_prompts_when_multiple_cargo_generate_is_found() -> anyhow::Result<()> {
897        let tmp = tmp_dir().unwrap();
898        create_file(&tmp, "dir1/Cargo.toml", "")?;
899        create_file(&tmp, "dir2/dir2_1/Cargo.toml", "")?;
900        create_file(&tmp, "dir2/dir2_2/cargo-generate.toml", "")?;
901        create_file(&tmp, "dir3/Cargo.toml", "")?;
902        create_file(&tmp, "dir4/cargo-generate.toml", "")?;
903
904        let actual = auto_locate_template_dir(tmp.path().to_path_buf(), &mut |slots| match &slots
905            .var_info
906        {
907            VarInfo::Bool { .. } => anyhow::bail!("Wrong prompt type"),
908            VarInfo::String { entry } => {
909                if let StringKind::Choices(choices) = entry.kind.clone() {
910                    let expected = vec![
911                        Path::new("dir2").join("dir2_2").to_string(),
912                        "dir4".to_string(),
913                    ];
914                    assert_eq!(expected, choices);
915                    Ok("dir4".to_string())
916                } else {
917                    anyhow::bail!("Missing choices")
918                }
919            }
920        })?
921        .canonicalize()?;
922        let expected = tmp.path().join("dir4").canonicalize()?;
923
924        assert_eq!(expected, actual);
925
926        Ok(())
927    }
928
929    pub trait PathString {
930        fn to_string(&self) -> String;
931    }
932
933    impl PathString for PathBuf {
934        fn to_string(&self) -> String {
935            self.as_path().to_string()
936        }
937    }
938
939    impl PathString for Path {
940        fn to_string(&self) -> String {
941            self.display().to_string()
942        }
943    }
944
945    pub fn create_file(
946        base_path: &TempDir,
947        path: impl AsRef<Path>,
948        contents: impl AsRef<str>,
949    ) -> anyhow::Result<()> {
950        let path = base_path.path().join(path);
951        if let Some(parent) = path.parent() {
952            fs::create_dir_all(parent)?;
953        }
954
955        fs::File::create(&path)?.write_all(contents.as_ref().as_ref())?;
956        Ok(())
957    }
958}