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_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_minijinja_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, TEMPLATE_SUFFIX};
55use env_logger::fmt::Formatter;
56use fs_err as fs;
57use hooks::{execute_hooks, RhaiHooksContext};
58use ignore_me::remove_dir_files;
59use interactive::{prompt_and_check_variable, LIST_SEP};
60use log::Record;
61use log::{info, warn};
62use project_variables::{StringEntry, StringKind, TemplateSlots, VarInfo};
63use std::{
64 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_template_object, set_project_name_variables, TemplateObjectResource};
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 strip_template_suffixes(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
279fn strip_template_suffixes(dir: impl AsRef<Path>) -> Result<()> {
281 for entry in fs::read_dir(dir.as_ref())? {
282 let entry = entry?;
283 let entry_type = entry.file_type()?;
284
285 if entry_type.is_dir() {
286 strip_template_suffixes(entry.path())?;
287 } else if entry_type.is_file() {
288 let path = entry.path().to_string_lossy().to_string();
289 if let Some(new_path) = path.clone().strip_suffix(TEMPLATE_SUFFIX) {
290 fs::rename(path, new_path)?;
291 }
292 }
293 }
294 Ok(())
295}
296
297fn resolve_template_dir(template_base_dir: &TempDir, subfolder: Option<&str>) -> Result<PathBuf> {
299 let template_dir = resolve_template_dir_subfolder(template_base_dir.path(), subfolder)?;
300 auto_locate_template_dir(template_dir, &mut |slots| {
301 prompt_and_check_variable(slots, None)
302 })
303}
304
305fn resolve_template_dir_subfolder(
307 template_base_dir: &Path,
308 subfolder: Option<impl AsRef<str>>,
309) -> Result<PathBuf> {
310 if let Some(subfolder) = subfolder {
311 let template_base_dir = fs::canonicalize(template_base_dir)?;
312 let template_dir = fs::canonicalize(template_base_dir.join(subfolder.as_ref()))
313 .with_context(|| {
314 format!(
315 "not able to find subfolder '{}' in source template",
316 subfolder.as_ref()
317 )
318 })?;
319
320 if !template_dir.starts_with(&template_base_dir) {
322 return Err(anyhow!(
323 "{} {} {}",
324 emoji::ERROR,
325 style("Subfolder Error:").bold().red(),
326 style("Invalid subfolder. Must be part of the template folder structure.")
327 .bold()
328 .red(),
329 ));
330 }
331
332 if !template_dir.is_dir() {
333 return Err(anyhow!(
334 "{} {} {}",
335 emoji::ERROR,
336 style("Subfolder Error:").bold().red(),
337 style("The specified subfolder must be a valid folder.")
338 .bold()
339 .red(),
340 ));
341 }
342
343 Ok(template_dir)
344 } else {
345 Ok(template_base_dir.to_owned())
346 }
347}
348
349fn auto_locate_template_dir(
351 template_base_dir: PathBuf,
352 prompt: &mut impl FnMut(&TemplateSlots) -> Result<String>,
353) -> Result<PathBuf> {
354 let config_paths = locate_template_configs(&template_base_dir)?;
355 match config_paths.len() {
356 0 => {
357 Ok(template_base_dir)
359 }
360 1 => {
361 resolve_configured_sub_templates(&template_base_dir.join(&config_paths[0]), prompt)
363 }
364 _ => {
365 let prompt_args = TemplateSlots {
368 prompt: "Which template should be expanded?".into(),
369 var_name: "Template".into(),
370 var_info: VarInfo::String {
371 entry: Box::new(StringEntry {
372 default: Some(config_paths[0].display().to_string()),
373 kind: StringKind::Choices(
374 config_paths
375 .into_iter()
376 .map(|p| p.display().to_string())
377 .collect(),
378 ),
379 regex: None,
380 }),
381 },
382 };
383 let path = prompt(&prompt_args)?;
384
385 auto_locate_template_dir(template_base_dir.join(path), prompt)
388 }
389 }
390}
391
392fn resolve_configured_sub_templates(
393 config_path: &Path,
394 prompt: &mut impl FnMut(&TemplateSlots) -> Result<String>,
395) -> Result<PathBuf> {
396 Config::from_path(&Some(config_path.join(CONFIG_FILE_NAME)))
397 .ok()
398 .and_then(|config| config.template)
399 .and_then(|config| config.sub_templates)
400 .map_or_else(
401 || Ok(PathBuf::from(config_path)),
402 |sub_templates| {
403 let prompt_args = TemplateSlots {
405 prompt: "Which sub-template should be expanded?".into(),
406 var_name: "Template".into(),
407 var_info: VarInfo::String {
408 entry: Box::new(StringEntry {
409 default: Some(sub_templates[0].clone()),
410 kind: StringKind::Choices(sub_templates.clone()),
411 regex: None,
412 }),
413 },
414 };
415 let path = prompt(&prompt_args)?;
416
417 auto_locate_template_dir(
420 resolve_template_dir_subfolder(config_path, Some(path))?,
421 prompt,
422 )
423 },
424 )
425}
426
427fn locate_template_file(
428 name: &str,
429 template_base_folder: impl AsRef<Path>,
430 template_folder: impl AsRef<Path>,
431) -> Result<PathBuf> {
432 let template_base_folder = template_base_folder.as_ref();
433 let mut search_folder = template_folder.as_ref().to_path_buf();
434 loop {
435 let file_path = search_folder.join::<&str>(name);
436 if file_path.exists() {
437 return Ok(file_path);
438 }
439 if search_folder == template_base_folder {
440 bail!("File not found within template");
441 }
442 search_folder = search_folder
443 .parent()
444 .ok_or_else(|| anyhow!("Reached root folder"))?
445 .to_path_buf();
446 }
447}
448
449fn expand_template(
450 template_dir: &Path,
451 config: &mut Config,
452 user_parsed_input: &UserParsedInput,
453 args: &GenerateArgs,
454) -> Result<PathBuf> {
455 let template_object = create_template_object(user_parsed_input)?;
456 let context = RhaiHooksContext {
457 template_object: template_object.clone(),
458 allow_commands: user_parsed_input.allow_commands(),
459 silent: user_parsed_input.silent(),
460 working_directory: template_dir.to_owned(),
461 destination_directory: user_parsed_input.destination().to_owned(),
462 };
463
464 execute_hooks(&context, &config.get_init_hooks())?;
470
471 let project_name_input = ProjectNameInput::try_from((&template_object, user_parsed_input))?;
472 let project_name = ProjectName::from((&project_name_input, user_parsed_input));
473 let crate_name = CrateName::from(&project_name_input);
474 let destination = ProjectDir::try_from((&project_name_input, user_parsed_input))?;
475 if !user_parsed_input.init() {
476 destination.create()?;
477 }
478
479 set_project_name_variables(&template_object, &destination, &project_name, &crate_name)?;
480
481 info!(
482 "{} {} {}",
483 emoji::WRENCH,
484 style(format!("Destination: {destination}")).bold(),
485 style("...").bold()
486 );
487 info!(
488 "{} {} {}",
489 emoji::WRENCH,
490 style(format!("project-name: {project_name}")).bold(),
491 style("...").bold()
492 );
493 project_variables::show_project_variables_with_value(&template_object, config);
494
495 info!(
496 "{} {} {}",
497 emoji::WRENCH,
498 style("Generating template").bold(),
499 style("...").bold()
500 );
501
502 fill_placeholders_and_merge_conditionals(
504 config,
505 &template_object,
506 user_parsed_input.template_values(),
507 args,
508 )?;
509 add_missing_provided_values(&template_object, user_parsed_input.template_values())?;
510
511 let context = RhaiHooksContext {
512 template_object: Arc::clone(&template_object),
513 destination_directory: destination.as_ref().to_owned(),
514 ..context
515 };
516
517 execute_hooks(&context, &config.get_pre_hooks())?;
519
520 let all_hook_files = config.get_hook_files();
522 let mut template_config = config.template.take().unwrap_or_default();
523 let preserve_whitespace = template_config.preserve_whitespace.unwrap_or(false);
524
525 ignore_me::remove_unneeded_files(template_dir, &template_config.ignore, args.verbose)?;
526 let mut pbar = progressbar::new();
527
528 let rhai_filter_files = Arc::new(Mutex::new(vec![]));
529 let rhai_engine = create_minijinja_engine(
530 template_dir.to_owned(),
531 template_object.clone(),
532 user_parsed_input.allow_commands(),
533 user_parsed_input.silent(),
534 rhai_filter_files.clone(),
535 preserve_whitespace,
536 );
537 let result = template::walk_dir(
538 &mut template_config,
539 template_dir,
540 &all_hook_files,
541 &template_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
566 .lock()
567 .unwrap()
568 .iter()
569 .cloned()
570 .collect::<Vec<_>>();
571 remove_dir_files(
572 all_hook_files
573 .into_iter()
574 .map(PathBuf::from)
575 .chain(rhai_filter_files),
576 false,
577 );
578
579 config.template.replace(template_config);
580 Ok(destination.as_ref().to_owned())
581}
582
583pub(crate) fn add_missing_provided_values(
588 template_object: &TemplateObjectResource,
589 template_values: &HashMap<String, toml::Value>,
590) -> Result<(), anyhow::Error> {
591 template_values.iter().try_for_each(|(k, v)| {
592 let map = template_object.lock().unwrap();
593 let borrowed = map.borrow();
594 if borrowed.contains_key(k.as_str()) {
595 return Ok(());
596 }
597 drop(borrowed);
598 drop(map);
599
600 let value = match v {
603 toml::Value::String(content) => serde_json::Value::from(content.clone()),
604 toml::Value::Boolean(content) => serde_json::Value::from(*content),
605 _ => anyhow::bail!(format!(
606 "{} {}",
607 emoji::ERROR,
608 style("Unsupported value type. Only Strings and Booleans are supported.")
609 .bold()
610 .red(),
611 )),
612 };
613 template_object
614 .lock()
615 .unwrap()
616 .borrow_mut()
617 .insert(k.clone(), value);
618 Ok(())
619 })?;
620 Ok(())
621}
622
623fn read_default_variable_value_from_template(slot: &TemplateSlots) -> Result<String, ()> {
624 let default_value = match &slot.var_info {
625 VarInfo::Bool {
626 default: Some(default),
627 } => default.to_string(),
628 VarInfo::String {
629 entry: string_entry,
630 } => match *string_entry.clone() {
631 StringEntry {
632 default: Some(default),
633 ..
634 } => default.clone(),
635 _ => return Err(()),
636 },
637 _ => return Err(()),
638 };
639 let (key, value) = (&slot.var_name, &default_value);
640 info!(
641 "{} {} (default value from template)",
642 emoji::WRENCH,
643 style(format!("{key}: {value:?}")).bold(),
644 );
645 Ok(default_value)
646}
647
648fn extract_toml_string(value: &toml::Value) -> Option<String> {
653 match value {
654 toml::Value::String(s) => Some(s.clone()),
655 toml::Value::Integer(s) => Some(s.to_string()),
656 toml::Value::Float(s) => Some(s.to_string()),
657 toml::Value::Boolean(s) => Some(s.to_string()),
658 toml::Value::Datetime(s) => Some(s.to_string()),
659 toml::Value::Array(s) => Some(
660 s.iter()
661 .filter_map(extract_toml_string)
662 .collect::<Vec<String>>()
663 .join(LIST_SEP),
664 ),
665 toml::Value::Table(_) => None,
666 }
667}
668
669fn fill_placeholders_and_merge_conditionals(
671 config: &mut Config,
672 template_object: &TemplateObjectResource,
673 template_values: &HashMap<String, toml::Value>,
674 args: &GenerateArgs,
675) -> Result<()> {
676 let mut conditionals = config.conditional.take().unwrap_or_default();
677
678 loop {
679 project_variables::fill_project_variables(template_object, config, |slot| {
681 let provided_value = template_values
682 .get(&slot.var_name)
683 .and_then(extract_toml_string);
684 if provided_value.is_none() && args.silent {
685 let default_value = match read_default_variable_value_from_template(slot) {
686 Ok(string) => string,
687 Err(()) => {
688 anyhow::bail!(ConversionError::MissingDefaultValueForPlaceholderVariable {
689 var_name: slot.var_name.clone()
690 })
691 }
692 };
693 interactive::variable(slot, Some(&default_value))
694 } else {
695 interactive::variable(slot, provided_value.as_ref())
696 }
697 })?;
698
699 let placeholders_changed = conditionals
700 .iter_mut()
701 .filter_map(|(key, cfg)| {
703 evaluate_script::<bool>(template_object, key)
704 .ok()
705 .filter(|&r| r)
706 .map(|_| cfg)
707 })
708 .map(|conditional_template_cfg| {
709 let template_cfg = config.template.get_or_insert_with(TemplateConfig::default);
711 if let Some(mut extras) = conditional_template_cfg.include.take() {
712 template_cfg
713 .include
714 .get_or_insert_with(Vec::default)
715 .append(&mut extras);
716 }
717 if let Some(mut extras) = conditional_template_cfg.exclude.take() {
718 template_cfg
719 .exclude
720 .get_or_insert_with(Vec::default)
721 .append(&mut extras);
722 }
723 if let Some(mut extras) = conditional_template_cfg.ignore.take() {
724 template_cfg
725 .ignore
726 .get_or_insert_with(Vec::default)
727 .append(&mut extras);
728 }
729 if let Some(extra_placeholders) = conditional_template_cfg.placeholders.take() {
730 match config.placeholders.as_mut() {
731 Some(placeholders) => {
732 for (k, v) in extra_placeholders.0 {
733 placeholders.0.insert(k, v);
734 }
735 }
736 None => {
737 config.placeholders = Some(extra_placeholders);
738 }
739 };
740 return true;
741 }
742 false
743 })
744 .fold(false, |acc, placeholders_changed| {
745 acc | placeholders_changed
746 });
747
748 if !placeholders_changed {
749 break;
750 }
751 }
752
753 Ok(())
754}
755
756fn check_cargo_generate_version(template_config: &Config) -> Result<(), anyhow::Error> {
757 if let Config {
758 template:
759 Some(config::TemplateConfig {
760 cargo_generate_version: Some(requirement),
761 ..
762 }),
763 ..
764 } = template_config
765 {
766 let version = semver::Version::parse(env!("CARGO_PKG_VERSION"))?;
767 if !requirement.matches(&version) {
768 bail!(
769 "{} {} {} {} {}",
770 emoji::ERROR,
771 style("Required cargo-generate version not met. Required:")
772 .bold()
773 .red(),
774 style(requirement).yellow(),
775 style(" was:").bold().red(),
776 style(version).yellow(),
777 );
778 }
779 }
780 Ok(())
781}
782
783#[derive(Debug)]
784struct ScopedWorkingDirectory(PathBuf);
785
786impl Default for ScopedWorkingDirectory {
787 fn default() -> Self {
788 Self(env::current_dir().unwrap())
789 }
790}
791
792impl Drop for ScopedWorkingDirectory {
793 fn drop(&mut self) {
794 env::set_current_dir(&self.0).unwrap();
795 }
796}
797
798#[cfg(test)]
799mod tests {
800 use crate::{
801 auto_locate_template_dir, extract_toml_string,
802 project_variables::{StringKind, VarInfo},
803 tmp_dir,
804 };
805 use anyhow::anyhow;
806 use std::{
807 fs,
808 io::Write,
809 path::{Path, PathBuf},
810 };
811 use tempfile::TempDir;
812
813 #[test]
814 fn auto_locate_template_returns_base_when_no_cargo_generate_is_found() -> anyhow::Result<()> {
815 let tmp = tmp_dir().unwrap();
816 create_file(&tmp, "dir1/Cargo.toml", "")?;
817 create_file(&tmp, "dir2/dir2_1/Cargo.toml", "")?;
818 create_file(&tmp, "dir3/Cargo.toml", "")?;
819
820 let actual =
821 auto_locate_template_dir(tmp.path().to_path_buf(), &mut |_slots| Err(anyhow!("test")))?
822 .canonicalize()?;
823 let expected = tmp.path().canonicalize()?;
824
825 assert_eq!(expected, actual);
826 Ok(())
827 }
828
829 #[test]
830 fn auto_locate_template_returns_path_when_single_cargo_generate_is_found() -> anyhow::Result<()>
831 {
832 let tmp = tmp_dir().unwrap();
833 create_file(&tmp, "dir1/Cargo.toml", "")?;
834 create_file(&tmp, "dir2/dir2_1/Cargo.toml", "")?;
835 create_file(&tmp, "dir2/dir2_2/cargo-generate.toml", "")?;
836 create_file(&tmp, "dir3/Cargo.toml", "")?;
837
838 let actual =
839 auto_locate_template_dir(tmp.path().to_path_buf(), &mut |_slots| Err(anyhow!("test")))?
840 .canonicalize()?;
841 let expected = tmp.path().join("dir2/dir2_2").canonicalize()?;
842
843 assert_eq!(expected, actual);
844 Ok(())
845 }
846
847 #[test]
848 fn auto_locate_template_can_resolve_configured_subtemplates() -> anyhow::Result<()> {
849 let tmp = tmp_dir().unwrap();
850 create_file(
851 &tmp,
852 "cargo-generate.toml",
853 indoc::indoc! {r#"
854 [template]
855 sub_templates = ["sub1", "sub2"]
856 "#},
857 )?;
858 create_file(&tmp, "sub1/Cargo.toml", "")?;
859 create_file(&tmp, "sub2/Cargo.toml", "")?;
860
861 let actual = auto_locate_template_dir(tmp.path().to_path_buf(), &mut |slots| match &slots
862 .var_info
863 {
864 VarInfo::Bool { .. } | VarInfo::Array { .. } => anyhow::bail!("Wrong prompt type"),
865 VarInfo::String { entry } => {
866 if let StringKind::Choices(choices) = entry.kind.clone() {
867 let expected = vec!["sub1".to_string(), "sub2".to_string()];
868 assert_eq!(expected, choices);
869 Ok("sub2".to_string())
870 } else {
871 anyhow::bail!("Missing choices")
872 }
873 }
874 })?
875 .canonicalize()?;
876 let expected = tmp.path().join("sub2").canonicalize()?;
877
878 assert_eq!(expected, actual);
879 Ok(())
880 }
881
882 #[test]
883 fn auto_locate_template_recurses_to_resolve_subtemplates() -> anyhow::Result<()> {
884 let tmp = tmp_dir().unwrap();
885 create_file(
886 &tmp,
887 "cargo-generate.toml",
888 indoc::indoc! {r#"
889 [template]
890 sub_templates = ["sub1", "sub2"]
891 "#},
892 )?;
893 create_file(&tmp, "sub1/Cargo.toml", "")?;
894 create_file(&tmp, "sub1/sub11/cargo-generate.toml", "")?;
895 create_file(
896 &tmp,
897 "sub1/sub12/cargo-generate.toml",
898 indoc::indoc! {r#"
899 [template]
900 sub_templates = ["sub122", "sub121"]
901 "#},
902 )?;
903 create_file(&tmp, "sub2/Cargo.toml", "")?;
904 create_file(&tmp, "sub1/sub11/Cargo.toml", "")?;
905 create_file(&tmp, "sub1/sub12/sub121/Cargo.toml", "")?;
906 create_file(&tmp, "sub1/sub12/sub122/Cargo.toml", "")?;
907
908 let mut prompt_num = 0;
909 let actual = auto_locate_template_dir(tmp.path().to_path_buf(), &mut |slots| match &slots
910 .var_info
911 {
912 VarInfo::Bool { .. } | VarInfo::Array { .. } => anyhow::bail!("Wrong prompt type"),
913 VarInfo::String { entry } => {
914 if let StringKind::Choices(choices) = entry.kind.clone() {
915 let (expected, answer) = match prompt_num {
916 0 => (vec!["sub1", "sub2"], "sub1"),
917 1 => (vec!["sub11", "sub12"], "sub12"),
918 2 => (vec!["sub122", "sub121"], "sub121"),
919 _ => panic!("Unexpected number of prompts"),
920 };
921 prompt_num += 1;
922 expected
923 .into_iter()
924 .zip(choices.iter())
925 .for_each(|(a, b)| assert_eq!(a, b));
926 Ok(answer.to_string())
927 } else {
928 anyhow::bail!("Missing choices")
929 }
930 }
931 })?
932 .canonicalize()?;
933
934 let expected = tmp
935 .path()
936 .join("sub1")
937 .join("sub12")
938 .join("sub121")
939 .canonicalize()?;
940
941 assert_eq!(expected, actual);
942 Ok(())
943 }
944
945 #[test]
946 fn auto_locate_template_prompts_when_multiple_cargo_generate_is_found() -> anyhow::Result<()> {
947 let tmp = tmp_dir().unwrap();
948 create_file(&tmp, "dir1/Cargo.toml", "")?;
949 create_file(&tmp, "dir2/dir2_1/Cargo.toml", "")?;
950 create_file(&tmp, "dir2/dir2_2/cargo-generate.toml", "")?;
951 create_file(&tmp, "dir3/Cargo.toml", "")?;
952 create_file(&tmp, "dir4/cargo-generate.toml", "")?;
953
954 let actual = auto_locate_template_dir(tmp.path().to_path_buf(), &mut |slots| match &slots
955 .var_info
956 {
957 VarInfo::Bool { .. } | VarInfo::Array { .. } => anyhow::bail!("Wrong prompt type"),
958 VarInfo::String { entry } => {
959 if let StringKind::Choices(choices) = entry.kind.clone() {
960 let expected = vec![
961 Path::new("dir2").join("dir2_2").to_string(),
962 "dir4".to_string(),
963 ];
964 assert_eq!(expected, choices);
965 Ok("dir4".to_string())
966 } else {
967 anyhow::bail!("Missing choices")
968 }
969 }
970 })?
971 .canonicalize()?;
972 let expected = tmp.path().join("dir4").canonicalize()?;
973
974 assert_eq!(expected, actual);
975
976 Ok(())
977 }
978
979 pub trait PathString {
980 fn to_string(&self) -> String;
981 }
982
983 impl PathString for PathBuf {
984 fn to_string(&self) -> String {
985 self.as_path().to_string()
986 }
987 }
988
989 impl PathString for Path {
990 fn to_string(&self) -> String {
991 self.display().to_string()
992 }
993 }
994
995 pub fn create_file(
996 base_path: &TempDir,
997 path: impl AsRef<Path>,
998 contents: impl AsRef<str>,
999 ) -> anyhow::Result<()> {
1000 let path = base_path.path().join(path);
1001 if let Some(parent) = path.parent() {
1002 fs::create_dir_all(parent)?;
1003 }
1004
1005 fs::File::create(&path)?.write_all(contents.as_ref().as_ref())?;
1006 Ok(())
1007 }
1008
1009 #[test]
1010 fn test_extract_toml_string() {
1011 assert_eq!(
1012 extract_toml_string(&toml::Value::Integer(42)),
1013 Some(String::from("42"))
1014 );
1015 assert_eq!(
1016 extract_toml_string(&toml::Value::Float(42.0)),
1017 Some(String::from("42"))
1018 );
1019 assert_eq!(
1020 extract_toml_string(&toml::Value::Boolean(true)),
1021 Some(String::from("true"))
1022 );
1023 assert_eq!(
1024 extract_toml_string(&toml::Value::Array(vec![
1025 toml::Value::Integer(1),
1026 toml::Value::Array(vec![toml::Value::Array(vec![toml::Value::Integer(2)])]),
1027 toml::Value::Integer(3),
1028 toml::Value::Integer(4),
1029 ])),
1030 Some(String::from("1,2,3,4"))
1031 );
1032 assert_eq!(
1033 extract_toml_string(&toml::Value::Table(toml::map::Map::new())),
1034 None
1035 );
1036 }
1037}