interactive_actions/
data.rs

1//!
2//! doc for module
3//!
4use anyhow::Result;
5use requestty::{Answer, Question};
6use std::collections::BTreeMap;
7
8use requestty_ui::backend::{Size, TestBackend};
9use requestty_ui::events::{KeyEvent, TestEvents};
10use serde_derive::{Deserialize, Serialize};
11use std::vec::IntoIter;
12
13fn default<T: Default + PartialEq>(t: &T) -> bool {
14    *t == Default::default()
15}
16
17#[doc(hidden)]
18pub type VarBag = BTreeMap<String, String>;
19
20///
21/// When in the workflow to hook the action
22///
23#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
24pub enum ActionHook {
25    /// Run after actions
26    #[default]
27    #[serde(rename = "after")]
28    After,
29
30    /// Run before actions
31    #[serde(rename = "before")]
32    Before,
33}
34///
35/// [`Action`] defines the action to run:
36/// * script
37/// * interaction
38/// * control flow and variable capture
39///
40#[derive(Clone, Debug, Serialize, Deserialize)]
41pub struct Action {
42    /// unique name of action
43    pub name: String,
44
45    /// interaction
46    #[serde(default)]
47    pub interaction: Option<Interaction>,
48
49    /// a run script
50    #[serde(default)]
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub run: Option<String>,
53
54    /// ignore exit code from the script, otherwise if error then exists
55    ///
56    #[serde(default)]
57    #[serde(skip_serializing_if = "default")]
58    pub ignore_exit: bool,
59
60    /// if confirm cancel, cancel all the rest of the actions and break out
61    #[serde(default)]
62    #[serde(skip_serializing_if = "default")]
63    pub break_if_cancel: bool,
64
65    /// captures the output of the script, otherwise, stream to screen in real time
66    #[serde(default)]
67    #[serde(skip_serializing_if = "default")]
68    pub capture: bool,
69
70    /// When to run this action
71    #[serde(default)]
72    #[serde(skip_serializing_if = "default")]
73    pub hook: ActionHook,
74}
75///
76/// result of the [`Action`]
77///
78#[derive(Clone, Debug, Serialize, Deserialize)]
79pub struct ActionResult {
80    /// name of action that was run
81    pub name: String,
82    /// result of run script
83    pub run: Option<RunResult>,
84    /// interaction response, if any
85    pub response: Response,
86}
87
88#[allow(missing_docs)]
89#[derive(Clone, Debug, Serialize, Deserialize)]
90pub struct RunResult {
91    pub script: String,
92    pub code: i32,
93    pub out: String,
94    pub err: String,
95}
96
97#[allow(missing_docs)]
98#[derive(Clone, Debug, Serialize, Deserialize)]
99pub enum InteractionKind {
100    #[serde(rename = "confirm")]
101    Confirm,
102    #[serde(rename = "input")]
103    Input,
104    #[serde(rename = "select")]
105    Select,
106}
107
108#[allow(missing_docs)]
109#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
110pub enum Response {
111    Text(String),
112    Cancel,
113    None,
114}
115
116///
117/// [`Interaction`] models an interactive session with a user declaratively
118/// You can pick from _confirm_, _input_, and other modes of prompting.
119#[derive(Clone, Debug, Serialize, Deserialize)]
120pub struct Interaction {
121    /// type of interaction
122    pub kind: InteractionKind,
123    /// what to ask the user
124    pub prompt: String,
125
126    /// if set, capture the value of answer, and set it to a variable name defined here
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub out: Option<String>,
129
130    /// define the set of options just for kind=select
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub options: Option<Vec<String>>,
133}
134impl Interaction {
135    fn update_varbag(&self, input: &str, varbag: Option<&mut VarBag>) {
136        varbag.map(|bag| {
137            self.out
138                .as_ref()
139                .map(|out| bag.insert(out.to_string(), input.to_string()))
140        });
141    }
142
143    /// Play an interaction
144    ///
145    /// # Errors
146    ///
147    /// This function will return an error if text input failed
148    pub fn play(
149        &self,
150        varbag: Option<&mut VarBag>,
151        events: Option<&mut TestEvents<IntoIter<KeyEvent>>>,
152    ) -> Result<Response> {
153        let question = self.to_question();
154        let answer = if let Some(events) = events {
155            let mut backend = TestBackend::new(Size::from((50, 20)));
156            requestty::prompt_one_with(question, &mut backend, events)
157        } else {
158            requestty::prompt_one(question)
159        }?;
160
161        Ok(match answer {
162            Answer::String(input) => {
163                self.update_varbag(&input, varbag);
164
165                Response::Text(input)
166            }
167            Answer::ListItem(selected) => {
168                self.update_varbag(&selected.text, varbag);
169                Response::Text(selected.text)
170            }
171            Answer::Bool(confirmed) if confirmed => {
172                let as_string = "true".to_string();
173                self.update_varbag(&as_string, varbag);
174                Response::Text(as_string)
175            }
176            _ => {
177                Response::Cancel
178                // not supported question types
179            }
180        })
181    }
182
183    /// Convert the interaction into a question
184    pub fn to_question(&self) -> Question<'_> {
185        match self.kind {
186            InteractionKind::Input => Question::input("question")
187                .message(self.prompt.clone())
188                .build(),
189            InteractionKind::Select => Question::select("question")
190                .message(self.prompt.clone())
191                .choices(self.options.clone().unwrap_or_default())
192                .build(),
193            InteractionKind::Confirm => Question::confirm("question")
194                .message(self.prompt.clone())
195                .build(),
196        }
197    }
198}