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