spin_templates/
interaction.rs

1use std::{collections::HashMap, path::Path};
2
3use crate::{
4    cancellable::Cancellable,
5    template::{TemplateParameter, TemplateParameterDataType},
6    Run,
7};
8
9use anyhow::anyhow;
10// use console::style;
11use 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}