Skip to main content

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