#![warn(missing_docs)]
#![allow(clippy::must_use_candidate)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::use_self)]
#![allow(clippy::missing_const_for_fn)]
pub mod data;
use anyhow::{Error, Result};
use data::{Action, ActionHook, ActionResult, Response, RunResult, VarBag};
use requestty_ui::events::{KeyEvent, TestEvents};
use run_script::IoOptions;
use std::path::Path;
use std::vec::IntoIter;
#[derive(Default)]
pub struct ActionRunner {
pub events: Option<TestEvents<IntoIter<KeyEvent>>>,
}
impl ActionRunner {
pub fn with_events(events: Vec<KeyEvent>) -> Self {
Self {
events: Some(TestEvents::new(events)),
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn run<P>(
&mut self,
actions: &[Action],
working_dir: Option<&Path>,
varbag: &mut VarBag,
hook: ActionHook,
progress: Option<P>,
) -> Result<Vec<ActionResult>>
where
P: Fn(&Action),
{
actions
.iter()
.filter(|action| action.hook == hook)
.map(|action| {
if let Some(ref progress) = progress {
progress(action);
}
let response = action
.interaction
.as_ref()
.map_or(Ok(Response::None), |interaction| {
interaction.play(Some(varbag), self.events.as_mut())
});
response.and_then(|r| match (r, action.run.as_ref()) {
(Response::Cancel, _) => {
if action.break_if_cancel {
Err(anyhow::anyhow!("stop requested (break_if_cancel)"))
} else {
Ok(ActionResult {
name: action.name.clone(),
run: None,
response: Response::Cancel,
})
}
}
(resp, None) => Ok(ActionResult {
name: action.name.clone(),
run: None,
response: resp,
}),
(resp, Some(run)) => {
let mut options = run_script::ScriptOptions::new();
options.working_directory = working_dir.map(std::path::Path::to_path_buf);
options.output_redirection = if action.capture {
IoOptions::Pipe
} else {
IoOptions::Inherit
};
options.print_commands = true;
let args = vec![];
let script = varbag.iter().fold(run.clone(), |acc, (k, v)| {
acc.replace(&format!("{{{{{}}}}}", k), v)
});
run_script::run(script.as_str(), &args, &options)
.map_err(Error::msg)
.and_then(|tup| {
if !action.ignore_exit && tup.0 != 0 {
anyhow::bail!(
"in action '{}': command returned exit code '{}'",
action.name,
tup.0
)
}
Ok(tup)
})
.map(|(code, out, err)| ActionResult {
name: action.name.clone(),
run: Some(RunResult {
script,
code,
out,
err,
}),
response: resp,
})
}
})
})
.collect::<Result<Vec<_>>>()
}
}
#[cfg(test)]
mod tests {
use super::*;
use insta::assert_debug_snapshot;
use requestty_ui::events::KeyCode;
#[test]
fn test_interaction() {
let actions_defs: Vec<Action> = serde_yaml::from_str(
r#"
- name: confirm-action
interaction:
kind: confirm
prompt: are you sure?
out: confirm
- name: input-action
interaction:
kind: input
prompt: which city?
default: dallas
out: city
- name: select-action
interaction:
kind: select
prompt: select transport
options:
- bus
- train
- walk
default: bus
"#,
)
.unwrap();
let events = vec![
KeyCode::Char('y').into(), KeyCode::Enter.into(), KeyCode::Char('t').into(), KeyCode::Char('l').into(), KeyCode::Char('v').into(), KeyCode::Enter.into(), KeyCode::Down.into(), KeyCode::Enter.into(), ];
let mut actions = ActionRunner::with_events(events);
let mut v = VarBag::new();
assert_debug_snapshot!(actions
.run(
&actions_defs,
Some(Path::new(".")),
&mut v,
ActionHook::After,
None::<&fn(&Action) -> ()>
)
.unwrap());
assert_debug_snapshot!(v);
}
#[test]
#[cfg(not(target_os = "windows"))]
fn test_run_script() {
let actions_defs: Vec<Action> = serde_yaml::from_str(
r#"
- name: input-action
interaction:
kind: input
prompt: which city?
default: dallas
out: city
run: echo {{city}}
capture: true
"#,
)
.unwrap();
let events = vec![
KeyCode::Char('t').into(), KeyCode::Char('l').into(), KeyCode::Char('v').into(), KeyCode::Enter.into(), ];
let mut actions = ActionRunner::with_events(events);
let mut v = VarBag::new();
insta::assert_yaml_snapshot!(actions
.run(
&actions_defs,
Some(Path::new(".")),
&mut v,
ActionHook::After,
None::<&fn(&Action) -> ()>)
.unwrap(), {
"[0].run.err" => ""
});
assert_debug_snapshot!(v);
}
}