spin_templates/
interaction.rs1use std::{collections::HashMap, path::Path};
2
3use crate::{
4 cancellable::Cancellable,
5 template::{TemplateParameter, TemplateParameterDataType},
6 Run,
7};
8
9use anyhow::anyhow;
10use dialoguer::{Confirm, Input, Select};
12
13pub(crate) trait InteractionStrategy {
14 fn allow_generate_into(&self, target_dir: &Path) -> Cancellable<(), anyhow::Error>;
15 fn populate_parameters(
16 &self,
17 run: &Run,
18 ) -> Cancellable<HashMap<String, String>, anyhow::Error> {
19 let mut values = HashMap::new();
20 for parameter in run.template.parameters(&run.options.variant) {
21 match self.populate_parameter(run, parameter) {
22 Cancellable::Ok(value) => {
23 values.insert(parameter.id().to_owned(), value);
24 }
25 Cancellable::Cancelled => return Cancellable::Cancelled,
26 Cancellable::Err(e) => return Cancellable::Err(e),
27 }
28 }
29 Cancellable::Ok(values)
30 }
31 fn populate_parameter(
32 &self,
33 run: &Run,
34 parameter: &TemplateParameter,
35 ) -> Cancellable<String, anyhow::Error>;
36}
37
38pub(crate) struct Interactive;
39pub(crate) struct Silent;
40
41impl InteractionStrategy for Interactive {
42 fn allow_generate_into(&self, target_dir: &Path) -> Cancellable<(), anyhow::Error> {
43 if !is_directory_empty(target_dir) {
44 let prompt = format!(
45 "Directory '{}' already contains other files. Generate into it anyway?",
46 target_dir.display()
47 );
48 match crate::interaction::confirm(&prompt) {
49 Ok(true) => Cancellable::Ok(()),
50 Ok(false) => Cancellable::Cancelled,
51 Err(e) => Cancellable::Err(e.into()),
52 }
53 } else {
54 Cancellable::Ok(())
55 }
56 }
57
58 fn populate_parameter(
59 &self,
60 run: &Run,
61 parameter: &TemplateParameter,
62 ) -> Cancellable<String, anyhow::Error> {
63 match run.options.values.get(parameter.id()) {
64 Some(s) => Cancellable::Ok(s.clone()),
65 None => match (run.options.accept_defaults, parameter.default_value()) {
66 (true, Some(v)) => Cancellable::Ok(v.to_string()),
67 _ => match crate::interaction::prompt_parameter(parameter) {
68 Some(v) => Cancellable::Ok(v),
69 None => Cancellable::Cancelled,
70 },
71 },
72 }
73 }
74}
75
76impl InteractionStrategy for Silent {
77 fn allow_generate_into(&self, target_dir: &Path) -> Cancellable<(), anyhow::Error> {
78 if is_directory_empty(target_dir) {
79 Cancellable::Ok(())
80 } else {
81 let err = anyhow!(
82 "Can't generate into {} as it already contains other files",
83 target_dir.display()
84 );
85 Cancellable::Err(err)
86 }
87 }
88
89 fn populate_parameter(
90 &self,
91 run: &Run,
92 parameter: &TemplateParameter,
93 ) -> Cancellable<String, anyhow::Error> {
94 match run.options.values.get(parameter.id()) {
95 Some(s) => Cancellable::Ok(s.clone()),
96 None => match (run.options.accept_defaults, parameter.default_value()) {
97 (true, Some(v)) => Cancellable::Ok(v.to_string()),
98 _ => Cancellable::Err(anyhow!("Parameter '{}' not provided", parameter.id())),
99 },
100 }
101 }
102}
103
104pub(crate) fn confirm(text: &str) -> dialoguer::Result<bool> {
105 Confirm::new().with_prompt(text).interact()
106}
107
108pub(crate) fn prompt_parameter(parameter: &TemplateParameter) -> Option<String> {
109 let prompt = parameter.prompt();
110 let default_value = parameter.default_value();
111
112 loop {
113 let input = match parameter.data_type() {
114 TemplateParameterDataType::String(constraints) => match &constraints.allowed_values {
115 Some(allowed_values) => ask_choice(prompt, default_value, allowed_values),
116 None => ask_free_text(prompt, default_value),
117 },
118 };
119
120 match input {
121 Ok(text) => match parameter.validate_value(text) {
122 Ok(text) => return Some(text),
123 Err(e) => {
124 println!("Invalid value: {}", e);
125 }
126 },
127 Err(e) => {
128 println!("Invalid value: {}", e);
129 }
130 }
131 }
132}
133
134fn ask_free_text(prompt: &str, default_value: &Option<String>) -> anyhow::Result<String> {
135 let mut input = Input::<String>::new();
136 input = input.with_prompt(prompt);
137 if let Some(s) = default_value {
138 input = input.default(s.to_owned());
139 }
140 let result = input.interact_text()?;
141 Ok(result)
142}
143
144fn ask_choice(
145 prompt: &str,
146 default_value: &Option<String>,
147 allowed_values: &[String],
148) -> anyhow::Result<String> {
149 let mut select = Select::new().with_prompt(prompt).items(allowed_values);
150 if let Some(s) = default_value {
151 if let Some(default_index) = allowed_values.iter().position(|item| item == s) {
152 select = select.default(default_index);
153 }
154 }
155 let selected_index = select.interact()?;
156 Ok(allowed_values[selected_index].clone())
157}
158
159fn is_directory_empty(path: &Path) -> bool {
160 if !path.exists() {
161 return true;
162 }
163 if !path.is_dir() {
164 return false;
165 }
166 match path.read_dir() {
167 Err(_) => false,
168 Ok(mut read_dir) => read_dir.next().is_none(),
169 }
170}