1use crate::{
2 emoji,
3 project_variables::{ArrayEntry, Prompt, StringEntry, StringKind, TemplateSlots, VarInfo},
4};
5use anyhow::{anyhow, bail, Result};
6use console::style;
7use dialoguer::{theme::ColorfulTheme, MultiSelect, Select};
8use dialoguer::{Editor, Input};
9use liquid_core::Value;
10use log::warn;
11use std::{
12 borrow::Cow,
13 io::{stdin, Read},
14 ops::Index,
15 str::FromStr,
16};
17
18pub const LIST_SEP: &str = ",";
19
20pub fn name() -> Result<String> {
21 let valid_ident = regex::Regex::new(r"^([a-zA-Z][a-zA-Z0-9_-]+)$")?;
22 let project_var = TemplateSlots {
23 var_name: "crate_name".into(),
24 prompt: "Project Name".into(),
25 var_info: VarInfo::String {
26 entry: Box::new(StringEntry {
27 default: None,
28 kind: StringKind::String,
29 regex: Some(valid_ident),
30 }),
31 },
32 };
33 prompt_and_check_variable(&project_var, None)
34}
35
36pub fn user_question(
37 prompt: &Prompt,
38 default: &Option<String>,
39 kind: &StringKind,
40) -> Result<String> {
41 match kind {
42 StringKind::String => {
43 let mut i = Input::<String>::new().with_prompt(&prompt.styled_with_default);
44 if let Some(s) = default {
45 i = i.default(s.to_owned());
46 }
47 i.interact().map_err(Into::<anyhow::Error>::into)
48 }
49 StringKind::Editor => {
50 println!("{} (in Editor)", prompt.styled_with_default);
51 Editor::new()
52 .edit(&prompt.with_default)?
53 .or_else(|| default.clone())
54 .ok_or(anyhow!("Aborted Editor without saving !"))
55 }
56 StringKind::Text => {
57 println!(
58 "{} (press Ctrl+d to stop reading)",
59 prompt.styled_with_default
60 );
61 let mut buffer = String::new();
62 stdin().read_to_string(&mut buffer)?;
63 Ok(buffer)
64 }
65 StringKind::Choices(_) => {
66 unreachable!("StringKind::Choices should be handled in the parent")
67 }
68 }
69}
70
71pub fn prompt_and_check_variable(
72 variable: &TemplateSlots,
73 provided_value: Option<String>,
74) -> Result<String> {
75 match &variable.var_info {
76 VarInfo::Bool { default } => handle_bool_input(provided_value, &variable.prompt, default),
77 VarInfo::String { entry } => match &entry.kind {
78 StringKind::Choices(choices) => handle_choice_input(
79 provided_value,
80 &variable.var_name,
81 choices,
82 entry,
83 &variable.prompt,
84 ),
85 StringKind::String | StringKind::Text | StringKind::Editor => {
86 handle_string_input(provided_value, &variable.var_name, entry, &variable.prompt)
87 }
88 },
89 VarInfo::Array { entry } => {
90 handle_multi_select_input(provided_value, &variable.var_name, entry, &variable.prompt)
91 }
92 }
93}
94
95pub fn variable(variable: &TemplateSlots, provided_value: Option<&impl ToString>) -> Result<Value> {
96 let user_entry = prompt_and_check_variable(variable, provided_value.map(|v| v.to_string()))?;
97 match &variable.var_info {
98 VarInfo::Bool { .. } => {
99 let as_bool = user_entry.parse::<bool>()?;
100 Ok(Value::Scalar(as_bool.into()))
101 }
102 VarInfo::String { .. } => Ok(Value::Scalar(user_entry.into())),
103 VarInfo::Array { .. } => {
104 let items = if user_entry.is_empty() {
105 Vec::new()
106 } else {
107 user_entry
108 .split(LIST_SEP)
109 .map(|s| Value::Scalar(s.to_string().into()))
110 .collect()
111 };
112
113 Ok(Value::Array(items))
114 }
115 }
116}
117
118fn handle_string_input(
119 provided_value: Option<String>,
120 var_name: &str,
121 entry: &StringEntry,
122 prompt: &Prompt,
123) -> Result<String> {
124 if let Some(value) = provided_value {
125 if entry
126 .regex
127 .as_ref()
128 .map(|ex| ex.is_match(&value))
129 .unwrap_or(true)
130 {
131 return Ok(value);
132 }
133 bail!(
134 "{} {} \"{}\" {}",
135 emoji::WARN,
136 style("Sorry,").bold().red(),
137 style(&value).bold().yellow(),
138 style(format!("is not a valid value for {var_name}"))
139 .bold()
140 .red()
141 )
142 };
143 let mut prompt: Cow<'_, Prompt> = Cow::Borrowed(prompt);
144 match &entry.regex {
145 Some(regex) => loop {
146 let user_entry = user_question(&prompt, &entry.default, &entry.kind)?;
147 if regex.is_match(&user_entry) {
148 break Ok(user_entry);
149 }
150 match entry.kind {
152 StringKind::Editor => {
153 prompt.to_mut().with_default = format!(
155 "{}: \"{user_entry}\" is not a valid value for `{var_name}`",
156 prompt
157 .with_default
158 .split_once(':')
159 .map(|t| t.0)
160 .unwrap_or(&prompt.with_default)
161 );
162 }
163 _ => {
164 warn!(
165 "{} \"{}\" {}",
166 style("Sorry,").bold().red(),
167 style(&user_entry).bold().yellow(),
168 style(format!("is not a valid value for {var_name}"))
169 .bold()
170 .red()
171 );
172 }
173 };
174 },
175 None => Ok(user_question(&prompt, &entry.default, &entry.kind)?),
176 }
177}
178
179fn handle_choice_input(
180 provided_value: Option<String>,
181 var_name: &str,
182 choices: &Vec<String>,
183 entry: &StringEntry,
184 prompt: &Prompt,
185) -> Result<String> {
186 match provided_value {
187 Some(value) => {
188 if choices.contains(&value) {
189 Ok(value)
190 } else {
191 bail!(
192 "{} {} \"{}\" {}",
193 emoji::WARN,
194 style("Sorry,").bold().red(),
195 style(&value).bold().yellow(),
196 style(format!("is not a valid value for {var_name}"))
197 .bold()
198 .red(),
199 )
200 }
201 }
202 None => {
203 let default = entry
204 .default
205 .as_ref()
206 .map_or(0, |default| choices.binary_search(default).unwrap_or(0));
207 let chosen = Select::with_theme(&ColorfulTheme::default())
208 .items(choices)
209 .with_prompt(&prompt.styled)
210 .default(default)
211 .interact()?;
212
213 Ok(choices.index(chosen).to_string())
214 }
215 }
216}
217
218fn parse_list(provided_value: &str) -> Vec<String> {
220 provided_value
221 .split(LIST_SEP)
222 .filter(|e| !e.is_empty())
223 .map(|s| s.to_string())
224 .collect()
225}
226
227fn check_provided_selections(
228 provided_value: &str,
229 choices: &[String],
230) -> Result<Vec<String>, Vec<String>> {
231 let list = parse_list(provided_value);
232 if list.is_empty() {
233 return Ok(Vec::new());
234 }
235 let (ok_entries, bad_entries): (Vec<String>, Vec<String>) =
236 list.iter().cloned().partition(|e| choices.contains(e));
237 if bad_entries.is_empty() {
238 Ok(ok_entries)
239 } else {
240 Err(bad_entries)
241 }
242}
243
244fn handle_multi_select_input(
245 provided_value: Option<String>,
246 var_name: &str,
247 entry: &ArrayEntry,
248 prompt: &Prompt,
249) -> Result<String> {
250 let val = match provided_value {
251 Some(value) => value,
253 None => {
255 let mut selected_by_default = Vec::<bool>::with_capacity(entry.choices.len());
256 match &entry.default {
257 None => {
259 selected_by_default.resize(entry.choices.len(), false);
260 }
261 Some(default_choices) => {
262 for choice in &entry.choices {
263 selected_by_default.push(default_choices.contains(choice));
264 }
265 }
266 };
267
268 let choice_indices = MultiSelect::with_theme(&ColorfulTheme::default())
269 .items(&entry.choices)
270 .with_prompt(&prompt.styled)
271 .defaults(&selected_by_default)
272 .interact()?;
273
274 choice_indices
275 .iter()
276 .filter_map(|idx| entry.choices.get(*idx))
277 .cloned()
278 .collect::<Vec<String>>()
279 .join(LIST_SEP)
280 }
281 };
282
283 match check_provided_selections(&val, &entry.choices) {
284 Ok(s) => Ok(s.join(LIST_SEP)),
285 Err(s) => {
286 let err_string = if s.len() > 1 {
287 format!("are not valid values for {var_name}")
288 } else {
289 format!("is not a valid value for {var_name}")
290 };
291
292 bail!(
293 "{} {} \"{}\" {}",
294 emoji::WARN,
295 style("Sorry,").bold().red(),
296 style(&s.join(LIST_SEP)).bold().yellow(),
297 style(err_string).bold().red(),
298 )
299 }
300 }
301}
302
303fn handle_bool_input(
304 provided_value: Option<String>,
305 prompt: &Prompt,
306 default: &Option<bool>,
307) -> Result<String> {
308 match provided_value {
309 Some(value) => {
310 let value = bool::from_str(&value.to_lowercase())?;
311 Ok(value.to_string())
312 }
313 None => {
314 let choices = [false.to_string(), true.to_string()];
315 let chosen = Select::with_theme(&ColorfulTheme::default())
316 .items(&choices)
317 .with_prompt(&prompt.styled)
318 .default(usize::from(default.unwrap_or(false)))
319 .interact()?;
320
321 Ok(choices.index(chosen).to_string())
322 }
323 }
324}