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 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
87pub 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
101pub fn generate(args: GenerateArgs) -> Result<PathBuf> {
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 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 }
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
281fn 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
299fn 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
307fn 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 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
351fn 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 Ok(template_base_dir)
361 }
362 1 => {
363 resolve_configured_sub_templates(&template_base_dir.join(&config_paths[0]), prompt)
365 }
366 _ => {
367 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 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 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 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 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 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 execute_hooks(&context, &config.get_pre_hooks())?;
521
522 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 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 execute_hooks(&context, &config.get_post_hooks())?;
563
564 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
586pub(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 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
650fn 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
671fn 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 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_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 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}