1#![doc = include_str!("../README.md")]
2mod git;
3mod helpers;
4
5use std::{
6 collections::BTreeMap,
7 env,
8 fs::{self, File},
9 io::{Read, Write},
10 path::{Path, PathBuf},
11 process::Command,
12 string::ToString,
13};
14
15use anyhow::{anyhow, Context, Result};
16use clap::Parser;
17use console::{Emoji, Style};
18use dialoguer::{Confirm, Input, MultiSelect, Select};
19use fs::OpenOptions;
20use globset::{Glob, GlobSetBuilder};
21use handlebars::Handlebars;
22use helpers::ForRangHelper;
23use serde::{Deserialize, Serialize};
24use walkdir::WalkDir;
25
26pub use toml::Value;
27pub const SCAFFOLD_FILENAME: &str = ".scaffold.toml";
28
29#[derive(Serialize, Deserialize)]
30pub struct ScaffoldDescription {
31 template: TemplateDescription,
32 #[serde(default)]
33 parameters: BTreeMap<String, Parameter>,
34 hooks: Option<Hooks>,
35 #[serde(skip)]
36 target_dir: Option<PathBuf>,
37 #[serde(skip)]
38 template_path: PathBuf,
39 #[serde(skip)]
40 force: bool,
41 #[serde(skip)]
42 append: bool,
43 #[serde(skip)]
44 project_name: Option<String>,
45 #[serde(skip)]
46 default_parameters: BTreeMap<String, Value>,
47}
48
49#[derive(Debug, Serialize, Deserialize, Clone)]
50pub struct TemplateDescription {
51 exclude: Option<Vec<String>>,
52 disable_templating: Option<Vec<String>>,
53 notes: Option<String>,
54}
55
56#[derive(Debug, Serialize, Deserialize, Clone)]
57pub struct Parameter {
58 message: String,
59 #[serde(default)]
60 required: bool,
61 r#type: ParameterType,
62 default: Option<Value>,
63 values: Option<Vec<Value>>,
64 tags: Option<Vec<String>>,
65}
66
67#[derive(Debug, Default, Serialize, Deserialize, Clone)]
68pub struct Hooks {
69 pre: Option<Vec<String>>,
70 post: Option<Vec<String>>,
71}
72
73#[derive(Debug, Serialize, Deserialize, Clone)]
74#[serde(rename_all = "lowercase")]
75pub enum ParameterType {
76 String,
77 Integer,
78 Float,
79 Boolean,
80 Select,
81 MultiSelect,
82}
83
84#[derive(Parser, Debug, Default)]
122#[command(author, version, about, long_about=None)]
123pub struct Opts {
124 #[arg(name = "template", required = true)]
126 template_path: PathBuf,
127
128 #[arg(name = "repository_template_path", short = 'r', long = "path")]
130 repository_template_path: Option<PathBuf>,
131
132 #[arg(name = "git_ref", short = 't', long = "git_ref")]
135 git_ref: Option<String>,
136
137 #[arg(name = "name", short = 'n', long = "name")]
139 project_name: Option<String>,
140
141 #[arg(name = "target_directory", short = 'd', long = "target_directory")]
143 target_dir: Option<PathBuf>,
144
145 #[arg(short = 'f', long = "force")]
147 force: bool,
148
149 #[arg(short = 'a', long = "append")]
151 append: bool,
152
153 #[arg(short = 'p', long = "passphrase")]
155 passphrase_needed: bool,
156
157 #[arg(short = 'k', long = "private_key_path")]
159 private_key_path: Option<PathBuf>,
160
161 #[arg(long = "param")]
163 parameters: Vec<String>,
164}
165
166impl Opts {
167 pub fn builder<T: Into<PathBuf>>(template_path: T) -> Self {
169 Self::default().template_path(template_path)
170 }
171
172 pub fn template_path<T: Into<PathBuf>>(mut self, path: T) -> Self {
174 let _ = std::mem::replace(&mut self.template_path, path.into());
175 self
176 }
177
178 pub fn repository_template_path<T: Into<PathBuf>>(mut self, path: T) -> Self {
180 let _ = self.repository_template_path.replace(path.into());
181 self
182 }
183
184 pub fn git_ref<T: Into<String>>(mut self, gitref: T) -> Self {
186 let _ = self.git_ref.replace(gitref.into());
187 self
188 }
189
190 pub fn project_name<T: Into<String>>(mut self, name: T) -> Self {
192 let _ = self.project_name.replace(name.into());
193 self
194 }
195
196 pub fn target_dir<T: Into<PathBuf>>(mut self, target_dir: T) -> Self {
198 let _ = self.target_dir.replace(target_dir.into());
199 self
200 }
201
202 pub fn force(mut self, force: bool) -> Self {
204 self.force = force;
205 self
206 }
207
208 pub fn append(mut self, append: bool) -> Self {
210 self.append = append;
211 self
212 }
213
214 pub fn passphrase_needed(mut self, needed: bool) -> Self {
216 self.passphrase_needed = needed;
217 self
218 }
219
220 pub fn private_key_path<T: Into<PathBuf>>(mut self, private_key_path: T) -> Self {
222 let _ = self.private_key_path.replace(private_key_path.into());
223 self
224 }
225
226 pub fn parameters<T: Into<String>>(mut self, params: Vec<T>) -> Self {
228 let _ = std::mem::replace(
229 &mut self.parameters,
230 params
231 .into_iter()
232 .map(|x| x.into())
233 .collect::<Vec<String>>(),
234 );
235 self
236 }
237}
238
239impl ScaffoldDescription {
240 pub fn new(opts: Opts) -> Result<Self> {
241 let mut default_parameters = BTreeMap::new();
242 for param in opts.parameters {
243 let split = param.splitn(2, '=').collect::<Vec<_>>();
244 if split.len() != 2 {
245 return Err(anyhow!("invalid argument: {}", param));
246 }
247 default_parameters.insert(split[0].to_string(), Value::String(split[1].to_string()));
248 }
249 if let Some(ref name) = opts.project_name {
250 default_parameters.insert("name".to_string(), Value::String(name.to_string()));
251 }
252
253 let mut template_path = opts.template_path.to_string_lossy().to_string();
254 let mut scaffold_desc: ScaffoldDescription = {
255 if template_path.ends_with(".git") {
256 let tmp_dir = env::temp_dir().join(format!("{:x}", md5::compute(&template_path)));
257 if tmp_dir.exists() {
258 fs::remove_dir_all(&tmp_dir)?;
259 }
260 fs::create_dir_all(&tmp_dir)?;
261 git::clone(
262 &template_path,
263 opts.git_ref.as_deref(),
264 &tmp_dir,
265 opts.private_key_path.as_deref(),
266 )?;
267 template_path = match opts.repository_template_path {
268 Some(sub_path) => tmp_dir.join(sub_path).to_string_lossy().to_string(),
269 None => tmp_dir.to_string_lossy().to_string(),
270 };
271 }
272 let mut scaffold_file =
273 File::open(PathBuf::from(&template_path).join(SCAFFOLD_FILENAME))
274 .with_context(|| format!("cannot open .scaffold.toml in {}", template_path))?;
275 let mut scaffold_desc_str = String::new();
276 scaffold_file.read_to_string(&mut scaffold_desc_str)?;
277 toml::from_str(&scaffold_desc_str)?
278 };
279
280 scaffold_desc.target_dir = opts.target_dir;
281 scaffold_desc.force = opts.force;
282 scaffold_desc.template_path = PathBuf::from(template_path);
283 scaffold_desc.project_name = opts.project_name;
284 scaffold_desc.append = opts.append;
285 scaffold_desc.default_parameters = default_parameters;
286
287 Ok(scaffold_desc)
288 }
289
290 pub fn name(&self) -> Option<String> {
291 self.project_name.clone()
292 }
293
294 fn create_dir(&self, name: &str) -> Result<PathBuf> {
295 let mut dir_path = self
296 .target_dir
297 .clone()
298 .unwrap_or_else(|| env::current_dir().unwrap_or_else(|_| ".".into()));
299
300 let cyan = Style::new().cyan();
301 if self.target_dir.is_none() {
302 dir_path = dir_path.join(name);
303 }
304 if dir_path.exists() {
305 if !self.force && !self.append {
306 return Err(anyhow!(
307 "cannot create {} because it already exists",
308 dir_path.to_string_lossy()
309 ));
310 } else if self.force {
311 println!(
312 "{} {}",
313 Emoji("🔄", ""),
314 cyan.apply_to("Override directory…"),
315 );
316 fs::remove_dir_all(&dir_path).with_context(|| "Cannot remove directory")?;
317 } else if self.append {
318 println!(
319 "{} {}",
320 Emoji("🔄", ""),
321 cyan.apply_to(format!(
322 "Append to directory {}…",
323 dir_path.to_string_lossy()
324 )),
325 );
326 }
327 } else {
328 println!(
329 "{} {}",
330 Emoji("🔄", ""),
331 cyan.apply_to(format!(
332 "Creating directory {}…",
333 dir_path.to_string_lossy()
334 )),
335 );
336 }
337 fs::create_dir_all(&dir_path).with_context(|| "Cannot create directory")?;
338 let path = fs::canonicalize(dir_path).with_context(|| "Cannot canonicalize path")?;
339
340 Ok(path)
341 }
342
343 pub fn fetch_parameters_value(&self) -> Result<BTreeMap<String, Value>> {
345 use std::collections::btree_map::Entry;
346
347 let mut parameters: BTreeMap<String, Value> = self.default_parameters.clone();
348 for (parameter_name, parameter) in &self.parameters {
349 if let Entry::Vacant(entry) = parameters.entry(parameter_name.clone()) {
350 entry.insert(parameter.to_value_interactive()?);
351 }
352 }
353
354 if let Entry::Vacant(entry) = parameters.entry("name".to_string()) {
355 let value = Parameter {
356 message: "What is the name of your generated project ?".to_string(),
357 required: true,
358 r#type: ParameterType::String,
359 default: None,
360 values: None,
361 tags: None,
362 }
363 .to_value_interactive()?;
364 entry.insert(value);
365 };
366
367 Ok(parameters)
368 }
369
370 pub fn scaffold(&self) -> Result<()> {
372 let mut parameters = self.default_parameters.clone();
373 parameters.append(&mut self.fetch_parameters_value()?);
374 self.internal_scaffold(parameters)
375 }
376
377 pub fn scaffold_with_parameters(&self, mut parameters: BTreeMap<String, Value>) -> Result<()> {
380 let mut default_parameters = self.default_parameters.clone();
381 if let Some(name) = &self.project_name {
382 parameters.insert("name".to_string(), Value::String(name.clone()));
383 } else {
384 return Err(anyhow!("project_name must be set"));
385 }
386
387 default_parameters.append(&mut parameters);
388 self.internal_scaffold(default_parameters)
389 }
390
391 fn internal_scaffold(&self, mut parameters: BTreeMap<String, Value>) -> Result<()> {
392 let excludes = match &self.template.exclude {
393 Some(exclude) => {
394 let mut builder = GlobSetBuilder::new();
395 for ex in exclude {
396 builder.add(Glob::new(ex.trim_start_matches("./"))?);
397 }
398
399 builder.build()?
400 }
401 None => GlobSetBuilder::new().build()?,
402 };
403 let disable_templating = match &self.template.disable_templating {
404 Some(exclude) => {
405 let mut builder = GlobSetBuilder::new();
406 for ex in exclude {
407 builder.add(Glob::new(ex.trim_start_matches("./"))?);
408 }
409
410 builder.build()?
411 }
412 None => GlobSetBuilder::new().build()?,
413 };
414
415 let name = parameters
416 .get("name")
417 .expect("project name must have been set. qed")
418 .as_str()
419 .expect("project name must be a string")
420 .to_string();
421 let dir_path = self.create_dir(&name)?;
422 parameters.insert(
423 "target_dir".to_string(),
424 Value::String(dir_path.to_str().unwrap_or_default().to_string()),
425 );
426
427 let mut template_engine = Handlebars::new();
428 template_engine.set_strict_mode(false);
429 #[cfg(feature = "helpers")]
430 handlebars_misc_helpers::setup_handlebars(&mut template_engine);
431 template_engine.register_helper("forRange", Box::new(ForRangHelper));
432
433 if let Some(Hooks {
435 pre: Some(commands),
436 ..
437 }) = &self.hooks
438 {
439 if !commands.is_empty() {
440 let cyan = Style::new().cyan();
441 println!(
442 "{} {}",
443 Emoji("🤖", ""),
444 cyan.apply_to("Triggering pre-hooks…"),
445 );
446 }
447 let commands = commands
448 .iter()
449 .map(|c| template_engine.render_template(c, ¶meters).ok())
450 .map(|v| v.unwrap())
451 .collect::<Vec<String>>();
452
453 self.run_hooks(&dir_path, &commands)?;
454 }
455
456 let entries = WalkDir::new(&self.template_path)
458 .into_iter()
459 .filter_entry(|entry| {
460 if entry
462 .path()
463 .components()
464 .any(|c| c == std::path::Component::Normal(".git".as_ref()))
465 {
466 return false;
467 }
468
469 if entry.depth() == 1 && entry.file_name() == SCAFFOLD_FILENAME {
470 return false;
471 }
472
473 !excludes.is_match(
474 entry
475 .path()
476 .strip_prefix(&self.template_path)
477 .unwrap_or_else(|_| entry.path()),
478 )
479 });
480
481 let cyan = Style::new().cyan();
482 println!("{} {}", Emoji("🔄", ""), cyan.apply_to("Templating files…"),);
483 for entry in entries {
484 let entry = entry.map_err(|e| anyhow!("cannot read entry : {}", e))?;
485 let entry_path = entry.path().strip_prefix(&self.template_path)?;
486
487 if entry_path == PathBuf::from("") {
488 continue;
489 }
490 if entry.file_type().is_dir() {
491 if entry.path().to_str() == Some(".") {
492 continue;
493 }
494
495 let entry_path = render_path(&template_engine, entry_path, ¶meters)?;
496
497 let dir_path_to_create = dir_path.join(&entry_path);
498 if dir_path_to_create.exists() && self.force {
499 fs::remove_dir_all(&dir_path_to_create)
500 .with_context(|| "Cannot remove directory")?;
501 }
502 if dir_path_to_create.exists() && self.append {
503 continue;
504 }
505 fs::create_dir(dir_path.join(entry_path))
506 .map_err(|e| anyhow!("cannot create dir : {}", e))?;
507 continue;
508 }
509
510 let filename = entry.path();
511 let mut content = Vec::new();
512 {
513 let mut file =
514 File::open(filename).map_err(|e| anyhow!("cannot open file : {}", e))?;
515 file.read_to_end(&mut content)
517 .map_err(|e| anyhow!("cannot read file {filename:?} : {}", e))?;
518 }
519 let (path, content) = if disable_templating.is_match(entry_path) {
520 (dir_path.join(entry_path), content)
521 } else {
522 let content = std::str::from_utf8(&content)
523 .map_err(|_| anyhow!("invalid UTF-8 in {entry_path:?}, consider disabling templating for this file"))?;
524 let rendered_content = template_engine
525 .render_template(content, ¶meters)
526 .map_err(|e| anyhow!("cannot render template {entry_path:?} : {}", e))?;
527
528 let rendered_path =
529 render_path(&template_engine, &dir_path.join(entry_path), ¶meters)?;
530 (rendered_path, rendered_content.into_bytes())
531 };
532
533 let filename_path = PathBuf::from(&path);
534 if filename_path.exists() && !self.force && self.append {
536 continue;
537 }
538
539 let permissions = entry
540 .metadata()
541 .map_err(|e| anyhow!("cannot get metadata for path : {}", e))?
542 .permissions();
543
544 let mut file = OpenOptions::new().write(true).create(true).open(&path)?;
545 file.set_permissions(permissions)
546 .map_err(|e| anyhow!("cannot set permission to file {:?} : {}", path, e))?;
547 file.write_all(&content)
548 .map_err(|e| anyhow!("cannot create file : {}", e))?;
549 }
550
551 let green = Style::new().green();
552 println!(
553 "{} Your project {} has been generated successfuly {}",
554 Emoji("✅", ""),
555 green.apply_to(name),
556 Emoji("🚀", "")
557 );
558
559 let yellow = Style::new().yellow();
560 println!(
561 "\n{}\n",
562 yellow.apply_to("-----------------------------------------------------"),
563 );
564
565 if let Some(notes) = &self.template.notes {
566 let rendered_notes = template_engine
567 .render_template(notes, ¶meters)
568 .map_err(|e| anyhow!("cannot render template for path : {}", e))?;
569 println!("{}", rendered_notes);
570 println!(
571 "\n{}\n",
572 yellow.apply_to("-----------------------------------------------------"),
573 );
574 }
575
576 if let Some(Hooks {
578 post: Some(commands),
579 ..
580 }) = &self.hooks
581 {
582 if !commands.is_empty() {
583 let cyan = Style::new().cyan();
584 println!(
585 "{} {}",
586 Emoji("🤖", ""),
587 cyan.apply_to("Triggering post-hooks…"),
588 );
589 let commands = commands
590 .iter()
591 .map(|c| template_engine.render_template(c, ¶meters).ok())
592 .map(|v| v.unwrap())
593 .collect::<Vec<String>>();
594
595 self.run_hooks(&dir_path, &commands)?;
596 }
597 }
598
599 Ok(())
600 }
601
602 fn run_hooks(&self, project_path: &Path, commands: &[String]) -> Result<()> {
603 let initial_path = std::env::current_dir()?;
604 std::env::set_current_dir(project_path).map_err(|e| {
606 anyhow!(
607 "cannot change directory to project path {:?}: {}",
608 &project_path,
609 e
610 )
611 })?;
612 let magenta = Style::new().magenta();
614 for cmd in commands {
615 println!("{} {}", Emoji("✨", ""), magenta.apply_to(cmd));
616 ScaffoldDescription::run_cmd(cmd)?;
617 }
618 std::env::set_current_dir(&initial_path).map_err(|e| {
620 anyhow!(
621 "cannot move back to original path {:?}: {}",
622 &initial_path,
623 e
624 )
625 })?;
626 Ok(())
627 }
628
629 pub fn run_cmd(cmd: &str) -> Result<()> {
630 let mut command = ScaffoldDescription::setup_cmd(cmd)?;
631 let mut child = command.spawn().expect("cannot execute command");
632 child.wait().expect("failed to wait on child process");
633 Ok(())
634 }
635
636 pub fn setup_cmd(cmd: &str) -> Result<Command> {
637 let splitted_cmd =
638 shell_words::split(cmd).map_err(|e| anyhow!("cannot split command line : {}", e))?;
639 if splitted_cmd.is_empty() {
640 anyhow::bail!("command argument is invalid: empty after splitting");
641 }
642 let mut command = Command::new(&splitted_cmd[0]);
643 if splitted_cmd.len() > 1 {
644 command.args(&splitted_cmd[1..]);
645 }
646 Ok(command)
647 }
648}
649
650fn render_path(
651 template_engine: &Handlebars,
652 path: &Path,
653 parameters: &BTreeMap<String, Value>,
654) -> Result<PathBuf> {
655 let mut output = PathBuf::new();
659 for component in path.components() {
660 match component {
661 std::path::Component::Normal(component) => {
662 let component = component
663 .to_str()
664 .ok_or_else(|| anyhow!("invalid Unicode path: {path:?}"))?;
665 let rendered = template_engine
666 .render_template(component, parameters)
667 .map_err(|e| anyhow!("cannot render template for path {path:?} : {}", e))?;
668 output.push(rendered);
669 }
670 component => output.push(component),
671 };
672 }
673 Ok(output)
674}
675
676impl Parameter {
677 fn to_value_interactive(&self) -> Result<toml::Value> {
678 let value = match self.r#type {
679 ParameterType::String => {
680 Value::String(Input::new().with_prompt(&self.message).interact()?)
681 }
682 ParameterType::Float => {
683 Value::Float(Input::<f64>::new().with_prompt(&self.message).interact()?)
684 }
685 ParameterType::Integer => {
686 Value::Integer(Input::<i64>::new().with_prompt(&self.message).interact()?)
687 }
688 ParameterType::Boolean => {
689 Value::Boolean(Confirm::new().with_prompt(&self.message).interact()?)
690 }
691 ParameterType::Select => {
692 let idx_selected = Select::new()
693 .items(
694 self.values
695 .as_ref()
696 .expect("cannot make a select parameter with empty values"),
697 )
698 .with_prompt(&self.message)
699 .default(0)
700 .interact()?;
701 self.values
702 .as_ref()
703 .expect("cannot make a select parameter with empty values")
704 .get(idx_selected)
705 .unwrap()
706 .clone()
707 }
708 ParameterType::MultiSelect => {
709 let idxs_selected = MultiSelect::new()
710 .items(
711 self.values
712 .as_ref()
713 .expect("cannot make a select parameter with empty values"),
714 )
715 .with_prompt(&self.message)
716 .interact()?;
717 let values = idxs_selected
718 .into_iter()
719 .map(|idx| {
720 self.values
721 .as_ref()
722 .expect("cannot make a select parameter with empty values")
723 .get(idx)
724 .unwrap()
725 .clone()
726 })
727 .collect();
728
729 Value::Array(values)
730 }
731 };
732 Ok(value)
733 }
734}
735
736#[cfg(test)]
737mod tests {
738 use crate::{render_path, BTreeMap, Handlebars};
739
740 use super::{Opts, ScaffoldDescription};
741 use std::fs::{remove_file, File};
742 use std::io::Write;
743 use std::path::Path;
744 use std::process::{Command, Stdio};
745
746 #[test]
747 #[cfg(windows)]
748 fn windows_paths_interpolation_works() {
749 let template_engine = Handlebars::new();
752
753 let mut parameters = BTreeMap::new();
754 parameters.insert("snake_name".to_string(), "tracing".to_string().into());
755
756 let path = Path::new(
757 "\\\\?\\C:\\Users\\Ignition\\AppData\\Local\\Temp\\router_scaffoldXwTZ11\\src\\plugins\\{{snake_name}}.rs"
758 );
759 let res = render_path(&template_engine, path, ¶meters).unwrap();
760
761 assert_eq!(Path::new("\\\\?\\C:\\Users\\Ignition\\AppData\\Local\\Temp\\router_scaffoldXwTZ11\\src\\plugins\\tracing.rs"), res);
762 }
763
764 #[test]
765 #[cfg(unix)]
766 fn unix_paths_interpolation_works() {
767 let template_engine = Handlebars::new();
770
771 let mut parameters = BTreeMap::new();
772 parameters.insert("snake_name".to_string(), "tracing".to_string().into());
773
774 let path = Path::new("/tmp/router_scaffoldXwTZ11/src/plugins/{{snake_name}}.rs");
775 let res = render_path(&template_engine, path, ¶meters).unwrap();
776
777 assert_eq!(
778 Path::new("/tmp/router_scaffoldXwTZ11/src/plugins/tracing.rs"),
779 res
780 );
781 }
782
783 #[test]
784 #[cfg(unix)]
785 fn unix_paths_dirs_work() {
786 let template_engine = Handlebars::new();
789
790 let mut parameters = BTreeMap::new();
791 parameters.insert("snake_name".to_string(), "tracing".to_string().into());
792 parameters.insert("directory_name".to_string(), "example".to_string().into());
793
794 let path = Path::new(
795 "/tmp/router_scaffoldXwTZ11/src/plugins/{{directory_name}}/{{snake_name}}.rs",
796 );
797 let res = render_path(&template_engine, path, ¶meters).unwrap();
798
799 assert_eq!(
800 Path::new("/tmp/router_scaffoldXwTZ11/src/plugins/example/tracing.rs"),
801 res
802 );
803 }
804
805 #[test]
806 fn split_and_run_cmd() {
807 let mut command = ScaffoldDescription::setup_cmd("ls -alh .").unwrap();
808 command.stdout(Stdio::null());
809 let mut child = command.spawn().expect("cannot execute command");
810 child.wait().expect("failed to wait on child process");
811
812 let mut command = ScaffoldDescription::setup_cmd("/bin/bash -c ls").unwrap();
813 command.stdout(Stdio::null());
814 let mut child = command.spawn().expect("cannot execute command");
815 child.wait().expect("failed to wait on child process");
816 }
817
818 #[test]
819 fn split_and_run_script() {
820 let script_name = "./test.sh";
821 let cmd = format!("/bin/bash -c {}", script_name);
822 {
823 let mut file = File::create(script_name).unwrap();
824 file.write_all(b"#!/bin/bash\nls .\nfree").unwrap();
825 Command::new("chmod")
826 .arg("+x")
827 .arg(script_name)
828 .output()
829 .expect("can't set execute perm on script file");
830 }
831 let mut command = ScaffoldDescription::setup_cmd(&cmd).unwrap();
832 command.stdout(Stdio::null());
833 let mut child = command.spawn().expect("cannot execute command");
834 child.wait().expect("failed to wait on child process");
835 remove_file(script_name).unwrap();
838 }
839
840 #[test]
841 fn test_build_opts_works() {
842 let opts = Opts::builder("/path/to/template");
843 assert_eq!(
844 opts.template_path,
845 std::path::PathBuf::from("/path/to/template")
846 );
847
848 assert!(opts.project_name.is_none());
850 let opts = opts.project_name("project");
851 assert_eq!(opts.project_name, Some("project".to_string()));
852
853 assert_eq!(
855 opts.template_path,
856 std::path::PathBuf::from("/path/to/template")
857 );
858 let opts = opts.template_path("/path/to/new-template");
859 assert_eq!(
860 opts.template_path,
861 std::path::PathBuf::from("/path/to/new-template")
862 );
863
864 assert!(opts.repository_template_path.is_none());
866 let opts = opts.repository_template_path("somepath");
867 assert_eq!(
868 opts.repository_template_path,
869 Some(std::path::PathBuf::from("somepath"))
870 );
871
872 assert!(opts.git_ref.is_none());
874 let opts = opts.git_ref("main");
875 assert_eq!(opts.git_ref, Some("main".to_string()));
876
877 assert!(opts.target_dir.is_none());
879 let opts = opts.target_dir("target");
880 assert_eq!(opts.target_dir, Some(std::path::PathBuf::from("target")));
881
882 assert!(!opts.append);
884 assert!(!opts.force);
885 assert!(!opts.passphrase_needed);
886 let opts = opts.append(true).force(true).passphrase_needed(true);
887 assert!(opts.append);
888 assert!(opts.force);
889 assert!(opts.passphrase_needed);
890
891 assert!(opts.private_key_path.is_none());
893 let opts = opts.private_key_path(".ssh/id_rsa");
894 assert_eq!(
895 opts.private_key_path,
896 Some(std::path::PathBuf::from(".ssh/id_rsa"))
897 );
898
899 assert!(opts.parameters.is_empty());
901 let opts = opts.parameters(vec!["key1=value1"]);
902 assert_eq!(opts.parameters, vec!["key1=value1".to_string()]);
903 }
904}