archetect_core/actions/
set.rs

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                // If there is an answer for this variable, it has an explicit value, and it is an acceptable answer,
24                // use that.
25                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                // If no answer was provided, there is an explicit value on the variable definition, and it is an
33                // acceptable value, use that.
34                match insert_answered_variable(archetect, identifier, value, &variable_info, context)? {
35                    None => continue,
36                    Some(warning) => warn!("{}", warning),
37                }
38            }
39        }
40
41        // Determine if a default can be provided.
42        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        // No answer or explict value provided.  Check to see if we're in headless mode before prompting for a value.
57        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        // If we've made it this far, there was not an acceptable answer or explicit value provided.  We need to prompt
69        // for a valid value.
70        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 the provided answer matches one of the enum values, use that; otherwise, we'll have to
100            // prompt the user for a valid answer
101            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 the provided answer is anything that resembled a boolean value, use that; otherwise, we'll
109            // have to prompt the user for a valid answer
110            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 the provided answer parses to an integer, use that; otherwise, we'll have to prompt the
121            // user for a proper integer
122            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        //        #[serde(flatten)]
356        items: LinkedHashMap<String, Box<VariableDescriptor>>,
357    },
358
359    #[serde(rename = "array!")]
360    Array {
361        prompt: String,
362        //        #[serde(flatten)]
363        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}