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