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