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