1use std::collections::{HashMap, HashSet};
2
3use linked_hash_map::LinkedHashMap;
4use log::{trace, warn};
5use crate::vendor::read_input::prelude::*;
6use serde_json::Value;
7
8use crate::config::{AnswerInfo, VariableInfo, VariableType};
9use crate::vendor::tera::Context;
10use crate::{Archetect, ArchetectError};
11
12const ACCEPTABLE_BOOLEANS: [&str; 8] = ["y", "yes", "true", "t", "n", "no", "false", "f"];
13
14pub fn populate_context(
15 archetect: &mut Archetect,
16 variables: &LinkedHashMap<String, VariableInfo>,
17 answers: &LinkedHashMap<String, AnswerInfo>,
18 context: &mut Context,
19) -> Result<(), ArchetectError> {
20 for (identifier, variable_info) in variables {
21 if let Some(answer) = answers.get(identifier) {
22 if let Some(value) = answer.value() {
23 match insert_answered_variable(archetect, identifier, value, &variable_info, context)? {
26 None => continue,
27 Some(warning) => warn!("{}", warning),
28 }
29 }
30 } else {
31 if let Some(value) = variable_info.value() {
32 match insert_answered_variable(archetect, identifier, value, &variable_info, context)? {
35 None => continue,
36 Some(warning) => warn!("{}", warning),
37 }
38 }
39 }
40
41 let default = if let Some(answer) = answers.get(identifier) {
43 if let Some(default) = answer.default() {
44 Some(archetect.render_string(default, context)?)
45 } else if let Some(default) = variable_info.default() {
46 Some(archetect.render_string(default, context)?)
47 } else {
48 None
49 }
50 } else if let Some(default) = variable_info.default() {
51 Some(archetect.render_string(default, context)?)
52 } else {
53 None
54 };
55
56 if archetect.headless() {
58 if let Some(default) = default {
59 match insert_answered_variable(archetect, identifier, &default, &variable_info, context)? {
60 None => continue,
61 Some(message) => {
62 return Err(ArchetectError::HeadlessInvalidDefault { identifier: identifier.to_owned(), default, message })
63 },
64 }
65 }
66 return Err(ArchetectError::HeadlessMissingAnswer(identifier.to_owned()));
67 }
68 let mut prompt = if let Some(prompt) = variable_info.prompt() {
71 format!("{} ", archetect.render_string(prompt.trim(), context)?)
72 } else {
73 format!("{}: ", identifier)
74 };
75
76 let value = match variable_info.variable_type() {
77 VariableType::Enum(values) => prompt_for_enum(&mut prompt, &values, &default),
78 VariableType::Bool => prompt_for_bool(&mut prompt, &default),
79 VariableType::Int => prompt_for_int(&mut prompt, &default),
80 VariableType::Array => prompt_for_list(archetect, context, &mut prompt, &default, variable_info)?,
81 VariableType::String => prompt_for_string(&mut prompt, &default, variable_info.required()),
82 };
83
84 if let Some(value) = value {
85 context.insert(identifier, &value);
86 }
87 }
88
89 Ok(())
90}
91
92fn insert_answered_variable(archetect: &mut Archetect, identifier: &str, value: &str, variable_info: &VariableInfo,
93 context: &mut Context) -> Result<Option<String>, ArchetectError> {
94
95 trace!("Setting variable answer {:?}={:?}", identifier, value);
96
97 match variable_info.variable_type() {
98 VariableType::Enum(options) => {
99 if options.contains(&value.to_owned()) {
102 context.insert(identifier, &archetect.render_string(value, context)?);
103 return Ok(None);
104 }
105 }
106 VariableType::Bool => {
107 let value = value.to_lowercase();
108 if ACCEPTABLE_BOOLEANS.contains(&value.as_str()) {
111 let value = match ACCEPTABLE_BOOLEANS.iter().position(|i| i == &value.as_str()).unwrap() {
112 0..=3 => true,
113 _ => false,
114 };
115 context.insert(identifier, &value);
116 return Ok(None);
117 }
118 }
119 VariableType::Int => {
120 if let Ok(value) = &archetect.render_string(value, context)?.parse::<i64>() {
123 context.insert(identifier, &value);
124 return Ok(None);
125 } else {
126 trace!("'{}' failed to parse as an int", value);
127 }
128 }
129 VariableType::String => {
130 context.insert(identifier, &archetect.render_string(value, context)?);
131 return Ok(None);
132 }
133 VariableType::Array => {
134 let values = convert_to_list(archetect, context, value)?;
135 if !values.is_empty() || !variable_info.required() {
136 trace!("Inserting {}={:?}", identifier, values);
137 context.insert(identifier, &Value::Array(values));
138 return Ok(None);
139 }
140 }
141 }
142
143 return Ok(Some(format!("{:?} is not a valid answer for {:?} with type {:?}.", value, identifier, variable_info.variable_type())));
144}
145
146fn convert_to_list(archetect: &mut Archetect, context: &Context, value: &str) -> Result<Vec<Value>, ArchetectError> {
147 let mut values = Vec::new();
148 let splits: Vec<&str> = value.split(",")
149 .map(|split| split.trim()).collect();
150 for split in splits {
151 if !split.is_empty() {
152 values.push(Value::String(archetect.render_string(split, context)?))
153 }
154 }
155 Ok(values)
156}
157
158fn prompt_for_string(prompt: &mut String, default: &Option<String>, required: bool) -> Option<Value> {
159 if let Some(default) = &default {
160 prompt.push_str(format!("[{}] ", default).as_str());
161 };
162 let mut input_builder = input::<String>().prompting_on_stderr().msg(&prompt);
163
164 if required {
165 input_builder = input_builder
166
167 .add_test(|value| value.len() > 0)
168 .repeat_msg(&prompt)
169 .err("Please provide a value.");
170 }
171
172 let value = if let Some(default) = &default {
173 input_builder.default(default.clone().to_owned()).get()
174 } else {
175 input_builder.get()
176 };
177 Some(Value::String(value))
178}
179
180fn prompt_for_int(prompt: &mut String, default: &Option<String>) -> Option<Value> {
181 let default = default.as_ref().map_or(None, |value| value.parse::<i64>().ok());
182
183 if let Some(default) = default {
184 prompt.push_str(format!("[{}] ", default).as_str());
185 }
186
187 let input_builder = input::<i64>()
188 .prompting_on_stderr()
189 .msg(&prompt)
190 .err("Please specify an integer.")
191 .repeat_msg(&prompt);
192
193 let value = if let Some(default) = default {
194 input_builder.default(default).get()
195 } else {
196 input_builder.get()
197 };
198
199 Some(Value::from(value))
200}
201
202fn prompt_for_bool(prompt: &mut String, default: &Option<String>) -> Option<Value> {
203 let default = default.as_ref().map_or(None, |value| {
204 let value = value.to_lowercase();
205 if ACCEPTABLE_BOOLEANS.contains(&value.as_str()) {
206 Some(value.to_owned())
207 } else {
208 None
209 }
210 });
211
212 if let Some(default) = default.clone() {
213 prompt.push_str(format!("[{}] ", default).as_str());
214 }
215
216 let input_builder = input::<String>()
217 .prompting_on_stderr()
218 .add_test(|value| ACCEPTABLE_BOOLEANS.contains(&value.to_lowercase().as_str()))
219 .msg(&prompt)
220 .err(format!("Please specify a value of {:?}.", ACCEPTABLE_BOOLEANS))
221 .repeat_msg(&prompt);
222
223 let value = if let Some(default) = default.clone() {
224 input_builder.default(default.to_owned()).get()
225 } else {
226 input_builder.get()
227 };
228
229 let value = match ACCEPTABLE_BOOLEANS.iter().position(|i| i == &value.as_str()).unwrap() {
230 0..=3 => true,
231 _ => false,
232 };
233
234 Some(Value::Bool(value))
235}
236
237fn prompt_for_list(
238 archetect: &mut Archetect,
239 context: &Context,
240 prompt: &mut String,
241 default: &Option<String>,
242 variable_info: &VariableInfo,
243) -> Result<Option<Value>, ArchetectError> {
244 if let Some(default) = &default {
245 prompt.push_str(format!("[{}] ", default).as_str());
246 };
247
248 eprintln!("{}", &prompt);
249
250 let mut results = vec![];
251
252 loop {
253 let requirements_met = if let Some(default) = default {
254 !default.trim().is_empty()
255 } else {
256 !results.is_empty()
257 };
258
259 let mut input_builder = input::<String>().prompting_on_stderr().msg(" - ");
260
261 if variable_info.required() {
262 input_builder = input_builder
263 .add_test(move |value| requirements_met || !value.trim().is_empty())
264 .err("This list requires at least one item.")
265 .repeat_msg(" - ")
266 }
267 let item = input_builder.get();
268
269 if item.trim().is_empty() {
270 break;
271 }
272
273 results.push(Value::String(item));
274 }
275
276 if results.len() > 0 || !variable_info.required() {
277 Ok(Some(Value::Array(results)))
278 } else if let Some(default) = default {
279 Ok(Some(Value::Array(convert_to_list(archetect, context, default)?)))
280 } else {
281 Ok(None)
282 }
283
284}
285
286fn prompt_for_enum(prompt: &mut String, options: &Vec<String>, default: &Option<String>) -> Option<Value> {
287 eprintln!("{}", &prompt);
288 let choices = options
289 .iter()
290 .enumerate()
291 .map(|(id, entry)| (id + 1, entry.clone()))
292 .collect::<HashMap<_, _>>();
293
294 for (id, option) in options.iter().enumerate() {
295 eprintln!("{:>2}) {}", id + 1, option);
296 }
297
298 let mut message = String::from("Select an entry: ");
299 if let Some(default) = default {
300 if options.contains(default) {
301 message.push_str(format!("[{}] ", default).as_str());
302 }
303 };
304
305 let test_values = choices.keys().map(|v| *v).collect::<HashSet<_>>();
306
307 let input_builder = input::<usize>()
308 .prompting_on_stderr()
309 .msg(&message)
310 .add_test(move |value| test_values.contains(value))
311 .err("Please enter the number of a selection from the list.")
312 .repeat_msg(&message);
313
314 let value = if let Some(default) = default {
315 if let Some(index) = options.iter().position(|e| e.eq(default)) {
316 input_builder.default(index + 1).get()
317 } else {
318 input_builder.get()
319 }
320 } else {
321 input_builder.get()
322 };
323
324 Some(Value::String(choices.get(&value).unwrap().to_owned()))
325}
326
327pub fn render_answers(
328 archetect: &mut Archetect,
329 answers: &LinkedHashMap<String, AnswerInfo>,
330 context: &Context,
331) -> Result<LinkedHashMap<String, AnswerInfo>, ArchetectError> {
332 let mut results = LinkedHashMap::new();
333 for (identifier, answer_info) in answers {
334 let mut result = AnswerInfo::new();
335 if let Some(value) = answer_info.value() {
336 result = result.with_value(archetect.render_string(value, context)?);
337 }
338 if let Some(prompt) = answer_info.prompt() {
339 result = result.with_prompt(archetect.render_string(prompt, context)?);
340 }
341 if let Some(default) = answer_info.default() {
342 result = result.with_default(archetect.render_string(default, context)?);
343 }
344 results.insert(identifier.to_owned(), result.build());
345 }
346 Ok(results)
347}
348
349#[derive(Debug, Serialize, Deserialize, Clone)]
350pub enum VariableDescriptor {
351 #[serde(rename = "object!")]
352 Object {
353 #[serde(skip_serializing_if = "Option::is_none")]
354 prompt: Option<String>,
355 items: LinkedHashMap<String, Box<VariableDescriptor>>,
357 },
358
359 #[serde(rename = "array!")]
360 Array {
361 prompt: String,
362 item: Box<VariableDescriptor>,
364 },
365
366 #[serde(rename = "string!")]
367 String {
368 prompt: String,
369 #[serde(skip_serializing_if = "Option::is_none")]
370 default: Option<String>,
371 },
372
373 #[serde(rename = "enum!")]
374 Enum {
375 prompt: String,
376 options: Vec<String>,
377 #[serde(skip_serializing_if = "Option::is_none")]
378 default: Option<String>,
379 },
380
381 #[serde(rename = "bool!")]
382 Bool {
383 prompt: String,
384 #[serde(skip_serializing_if = "Option::is_none")]
385 default: Option<String>,
386 },
387
388 #[serde(rename = "number!")]
389 Number {
390 prompt: String,
391 #[serde(skip_serializing_if = "Option::is_none")]
392 default: Option<String>,
393 },
394
395 #[serde(rename = "json!")]
396 Json { content: String, render: Option<bool> },
397}
398
399#[cfg(test)]
400mod tests {
401 use crate::actions::set::VariableDescriptor;
402 use linked_hash_map::LinkedHashMap;
403
404 #[test]
405 fn test_serialize() {
406 let object = VariableDescriptor::Object {
407 prompt: Some("Schema:".to_string()),
408 items: values_map(vec![(
409 "tables",
410 VariableDescriptor::Array {
411 prompt: "Tables: ".to_string(),
412 item: Box::new(VariableDescriptor::Object {
413 prompt: None,
414 items: values_map(vec![
415 (
416 "name",
417 VariableDescriptor::String {
418 prompt: "Table Name: ".to_string(),
419 default: None,
420 },
421 ),
422 (
423 "fields",
424 VariableDescriptor::Array {
425 prompt: "Fields: ".to_string(),
426 item: Box::new(VariableDescriptor::Object {
427 prompt: None,
428 items: values_map(vec![
429 (
430 "type",
431 VariableDescriptor::Enum {
432 prompt: "Field Type: ".to_string(),
433 options: vec!["String".to_owned(), "Integer".to_owned()],
434 default: Some("String".to_owned()),
435 },
436 ),
437 (
438 "name",
439 VariableDescriptor::String {
440 prompt: "Field Name: ".to_string(),
441 default: None,
442 },
443 ),
444 ]),
445 }),
446 },
447 ),
448 ]),
449 }),
450 },
451 )]),
452 };
453
454 let yaml = serde_yaml::to_string(&object).unwrap();
455 println!("{}", yaml);
456 }
457
458
459 fn values_map<K: Into<String>, V>(values: Vec<(K, V)>) -> LinkedHashMap<String, Box<V>> {
460 let mut results = LinkedHashMap::new();
461 for (identifier, value) in values {
462 results.insert(identifier.into(), Box::new(value));
463 }
464 results
465 }
466}