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