use color_eyre::{eyre::Report, Result};
use inquire::validator::Validation;
use inquire::{Confirm, MultiSelect, Select, Text};
#[cfg(test)]
use std::cell::RefCell;
use std::fmt::Display;
pub trait InquireApi {
fn text(&self, message: &str, help: Option<&str>) -> Result<String>;
fn text_with_validation(
&self,
message: &str,
help: Option<&str>,
validator: impl Fn(&str) -> Result<bool, String> + Clone + 'static,
error_message: &str,
) -> Result<String>;
fn select<T: ToString + Display + Clone>(
&self,
message: &str,
options: &[T],
help: Option<&str>,
) -> Result<usize>;
fn confirm(&self, message: &str, default: bool) -> Result<bool>;
fn multiselect<T: ToString + Display + Clone>(
&self,
message: &str,
options: &[T],
defaults: &[usize],
help: Option<&str>,
) -> Result<Vec<usize>>;
}
pub struct RealInquire;
impl InquireApi for RealInquire {
fn text(&self, message: &str, help: Option<&str>) -> Result<String> {
let mut prompt = Text::new(message);
if let Some(h) = help {
prompt = prompt.with_help_message(h);
}
prompt
.prompt()
.map_err(|e| Report::msg(format!("Text input error: {e}")))
}
fn text_with_validation(
&self,
message: &str,
help: Option<&str>,
validator: impl Fn(&str) -> Result<bool, String> + Clone + 'static,
error_message: &str,
) -> Result<String> {
let error_msg = error_message.to_string();
let validation_fn = move |input: &str| match validator(input) {
Ok(true) => Ok(Validation::Valid),
Ok(false) | Err(_) => Ok(Validation::Invalid(error_msg.clone().into())),
};
let mut prompt = Text::new(message);
prompt = prompt.with_validator(validation_fn);
if let Some(h) = help {
prompt = prompt.with_help_message(h);
}
prompt
.prompt()
.map_err(|e| Report::msg(format!("Text input error: {e}")))
}
fn select<T: ToString + Display + Clone>(
&self,
message: &str,
options: &[T],
help: Option<&str>,
) -> Result<usize> {
let mut prompt = Select::new(message, options.to_vec());
if let Some(h) = help {
prompt = prompt.with_help_message(h);
}
prompt
.prompt()
.map_err(|e| Report::msg(format!("Selection error: {e}")))
.map(|selected| {
options
.iter()
.position(|item| item.to_string() == selected.to_string())
.unwrap_or(0) })
}
fn confirm(&self, message: &str, default: bool) -> Result<bool> {
Confirm::new(message)
.with_default(default)
.prompt()
.map_err(|e| Report::msg(format!("Confirmation error: {e}")))
}
fn multiselect<T: ToString + Display + Clone>(
&self,
message: &str,
options: &[T],
defaults: &[usize],
help: Option<&str>,
) -> Result<Vec<usize>> {
let mut prompt = MultiSelect::new(message, options.to_vec());
if !defaults.is_empty() {
prompt = prompt.with_default(defaults);
}
if let Some(h) = help {
prompt = prompt.with_help_message(h);
}
prompt
.prompt()
.map_err(|e| Report::msg(format!("MultiSelect error: {e}")))
.map(|selected| {
selected
.iter()
.filter_map(|item| {
options
.iter()
.position(|opt| opt.to_string() == item.to_string())
})
.collect()
})
}
}
#[cfg(test)]
pub struct TestInquire {
text_responses: RefCell<std::collections::VecDeque<Result<String>>>,
select_responses: RefCell<std::collections::VecDeque<Result<usize>>>,
confirm_responses: RefCell<std::collections::VecDeque<Result<bool>>>,
multiselect_responses: RefCell<std::collections::VecDeque<Result<Vec<usize>>>>,
}
#[cfg(test)]
impl TestInquire {
pub fn new() -> Self {
Self {
text_responses: RefCell::new(std::collections::VecDeque::new()),
select_responses: RefCell::new(std::collections::VecDeque::new()),
confirm_responses: RefCell::new(std::collections::VecDeque::new()),
multiselect_responses: RefCell::new(std::collections::VecDeque::new()),
}
}
pub fn queue_text_response(&self, response: Result<String>) {
self.text_responses.borrow_mut().push_back(response);
}
pub fn queue_select_response(&self, response: Result<usize>) {
self.select_responses.borrow_mut().push_back(response);
}
pub fn queue_confirm_response(&self, response: Result<bool>) {
self.confirm_responses.borrow_mut().push_back(response);
}
pub fn queue_multiselect_response(&self, response: Result<Vec<usize>>) {
self.multiselect_responses.borrow_mut().push_back(response);
}
pub fn add_text(&self, text: &str) {
self.queue_text_response(Ok(text.to_string()));
}
pub fn add_select(&self, idx: usize) {
self.queue_select_response(Ok(idx));
}
pub fn add_confirm(&self, value: bool) {
self.queue_confirm_response(Ok(value));
}
pub fn add_multiselect(&self, indices: Vec<usize>) {
self.queue_multiselect_response(Ok(indices));
}
fn next_text_response(&self) -> Result<String> {
self.text_responses
.borrow_mut()
.pop_front()
.unwrap_or_else(|| Err(Report::msg("No more mock text responses")))
}
fn next_select_response(&self) -> Result<usize> {
self.select_responses
.borrow_mut()
.pop_front()
.unwrap_or_else(|| Err(Report::msg("No more mock select responses")))
}
fn next_confirm_response(&self) -> Result<bool> {
self.confirm_responses
.borrow_mut()
.pop_front()
.unwrap_or_else(|| Err(Report::msg("No more mock confirm responses")))
}
fn next_multiselect_response(&self) -> Result<Vec<usize>> {
self.multiselect_responses
.borrow_mut()
.pop_front()
.unwrap_or_else(|| Err(Report::msg("No more mock multiselect responses")))
}
}
#[cfg(test)]
impl InquireApi for TestInquire {
fn text(&self, _message: &str, _help: Option<&str>) -> Result<String> {
self.next_text_response()
}
fn text_with_validation(
&self,
_message: &str,
_help: Option<&str>,
validator: impl Fn(&str) -> Result<bool, String> + Clone + 'static,
error_message: &str,
) -> Result<String> {
let response = self.next_text_response()?;
if let Ok(valid) = validator(&response) {
if valid {
Ok(response)
} else {
Err(Report::msg(format!("Validation failed: {error_message}")))
}
} else {
Err(Report::msg(format!("Validation failed: {error_message}")))
}
}
fn select<T: ToString + Display + Clone>(
&self,
_message: &str,
_options: &[T],
_help: Option<&str>,
) -> Result<usize> {
self.next_select_response()
}
fn confirm(&self, _message: &str, _default: bool) -> Result<bool> {
self.next_confirm_response()
}
fn multiselect<T: ToString + Display + Clone>(
&self,
_message: &str,
_options: &[T],
_defaults: &[usize],
_help: Option<&str>,
) -> Result<Vec<usize>> {
self.next_multiselect_response()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_test_inquire_text() {
let test_inquire = TestInquire::new();
test_inquire.add_text("sample text");
let result = test_inquire.text("Test prompt", None);
assert!(result.is_ok());
if let Ok(text) = result {
assert_eq!(text, "sample text");
} else {
panic!("Result should be Ok but was Err");
}
}
#[test]
fn test_test_inquire_text_error() {
let test_inquire = TestInquire::new();
let result = test_inquire.text("Test prompt", None);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("No more mock text responses"));
}
#[test]
fn test_test_inquire_text_with_validation() {
let test_inquire = TestInquire::new();
test_inquire.add_text("valid-text");
let validator = |input: &str| -> Result<bool, String> {
if input.is_empty() {
Ok(false)
} else {
Ok(true)
}
};
let result = test_inquire.text_with_validation(
"Test prompt",
None,
validator,
"Input cannot be empty",
);
assert!(result.is_ok());
if let Ok(text) = result {
assert_eq!(text, "valid-text");
} else {
panic!("Result should be Ok but was Err");
}
test_inquire.add_text("");
let result = test_inquire.text_with_validation(
"Test prompt",
None,
validator,
"Input cannot be empty",
);
assert!(result.is_err());
if let Err(err) = result {
assert!(err.to_string().contains("Validation failed"));
} else {
panic!("Result should be Err but was Ok");
}
}
#[test]
fn test_test_inquire_text_with_validation_error_response() {
let test_inquire = TestInquire::new();
test_inquire.queue_text_response(Err(Report::msg("Mock text error")));
let validator = |_: &str| -> Result<bool, String> { Ok(true) };
let result = test_inquire.text_with_validation(
"Test prompt",
None,
validator,
"Input cannot be empty",
);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Mock text error"));
}
#[test]
fn test_test_inquire_text_with_validation_error_from_validator() {
let test_inquire = TestInquire::new();
test_inquire.add_text("invalid");
let validator = |_: &str| -> Result<bool, String> { Err("Validator error".to_string()) };
let result = test_inquire.text_with_validation(
"Test prompt",
None,
validator,
"Custom error message",
);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Validation failed"));
}
#[test]
fn test_test_inquire_select() {
let test_inquire = TestInquire::new();
test_inquire.add_select(1);
let options = ["Option 1", "Option 2", "Option 3"];
let result = test_inquire.select("Test prompt", &options, None);
assert!(result.is_ok());
if let Ok(selected) = result {
assert_eq!(selected, 1);
} else {
panic!("Result should be Ok but was Err");
}
}
#[test]
fn test_test_inquire_select_error() {
let test_inquire = TestInquire::new();
let options = ["Option 1", "Option 2"];
let result = test_inquire.select("Test prompt", &options, None);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("No more mock select responses"));
}
#[test]
fn test_test_inquire_select_with_help() {
let test_inquire = TestInquire::new();
test_inquire.add_select(2);
let options = ["Option 1", "Option 2", "Option 3"];
let result = test_inquire.select("Test prompt", &options, Some("Help text"));
assert!(result.is_ok());
if let Ok(selected) = result {
assert_eq!(selected, 2);
} else {
panic!("Result should be Ok but was Err");
}
}
#[test]
fn test_test_inquire_confirm() {
let test_inquire = TestInquire::new();
test_inquire.add_confirm(true);
let result = test_inquire.confirm("Test prompt", false);
assert!(result.is_ok());
if let Ok(confirmed) = result {
assert!(confirmed);
} else {
panic!("Result should be Ok but was Err");
}
}
#[test]
fn test_test_inquire_confirm_error() {
let test_inquire = TestInquire::new();
let result = test_inquire.confirm("Test prompt", true);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("No more mock confirm responses"));
}
#[test]
fn test_test_inquire_confirm_false() {
let test_inquire = TestInquire::new();
test_inquire.add_confirm(false);
let result = test_inquire.confirm("Test prompt", true);
assert!(result.is_ok());
if let Ok(confirmed) = result {
assert!(!confirmed);
} else {
panic!("Result should be Ok but was Err");
}
}
#[test]
fn test_test_inquire_multiselect() {
let test_inquire = TestInquire::new();
test_inquire.add_multiselect(vec![0, 2]);
let options = ["Option 1", "Option 2", "Option 3"];
let result = test_inquire.multiselect("Test prompt", &options, &[], None);
assert!(result.is_ok());
if let Ok(selected) = result {
assert_eq!(selected, vec![0, 2]);
} else {
panic!("Result should be Ok but was Err");
}
}
#[test]
fn test_test_inquire_multiselect_error() {
let test_inquire = TestInquire::new();
let options = ["Option 1", "Option 2"];
let result = test_inquire.multiselect("Test prompt", &options, &[], None);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("No more mock multiselect responses"));
}
#[test]
fn test_test_inquire_multiselect_with_defaults_and_help() {
let test_inquire = TestInquire::new();
test_inquire.add_multiselect(vec![1, 3]);
let options = ["Option 1", "Option 2", "Option 3", "Option 4"];
let result = test_inquire.multiselect(
"Test prompt",
&options,
&[0, 2], Some("Help text"),
);
assert!(result.is_ok());
if let Ok(selected) = result {
assert_eq!(selected, vec![1, 3]);
} else {
panic!("Result should be Ok but was Err");
}
}
#[test]
fn test_test_inquire_queue_error_responses() {
let test_inquire = TestInquire::new();
test_inquire.queue_text_response(Err(Report::msg("Text error")));
test_inquire.queue_select_response(Err(Report::msg("Select error")));
test_inquire.queue_confirm_response(Err(Report::msg("Confirm error")));
test_inquire.queue_multiselect_response(Err(Report::msg("Multiselect error")));
assert!(test_inquire
.text("", None)
.unwrap_err()
.to_string()
.contains("Text error"));
let options = ["A", "B"];
assert!(test_inquire
.select("", &options, None)
.unwrap_err()
.to_string()
.contains("Select error"));
assert!(test_inquire
.confirm("", false)
.unwrap_err()
.to_string()
.contains("Confirm error"));
assert!(test_inquire
.multiselect("", &options, &[], None)
.unwrap_err()
.to_string()
.contains("Multiselect error"));
}
#[test]
fn test_test_inquire_empty_queue_handling() {
let test_inquire = TestInquire::new();
test_inquire.add_text("one text");
test_inquire.add_select(0);
test_inquire.add_confirm(true);
test_inquire.add_multiselect(vec![1]);
let _text = test_inquire.text("", None);
let options = ["A", "B"];
let _select = test_inquire.select("", &options, None);
let _confirm = test_inquire.confirm("", false);
let _multi = test_inquire.multiselect("", &options, &[], None);
assert!(test_inquire.text("", None).is_err());
assert!(test_inquire.select("", &options, None).is_err());
assert!(test_inquire.confirm("", false).is_err());
assert!(test_inquire.multiselect("", &options, &[], None).is_err());
}
#[test]
fn test_test_inquire_multiple_text_responses() {
let test_inquire = TestInquire::new();
test_inquire.add_text("first response");
test_inquire.add_text("second response");
test_inquire.add_text("third response");
let result1 = test_inquire.text("Prompt 1", None);
assert!(result1.is_ok());
if let Ok(text) = result1 {
assert_eq!(text, "first response");
}
let result2 = test_inquire.text("Prompt 2", Some("Help"));
assert!(result2.is_ok());
if let Ok(text) = result2 {
assert_eq!(text, "second response");
}
let result3 = test_inquire.text("Prompt 3", None);
assert!(result3.is_ok());
if let Ok(text) = result3 {
assert_eq!(text, "third response");
}
}
#[test]
fn test_test_inquire_with_help_text() {
let test_inquire = TestInquire::new();
test_inquire.add_text("response with help");
let result = test_inquire.text("Test prompt", Some("This is helpful text"));
assert!(result.is_ok());
if let Ok(text) = result {
assert_eq!(text, "response with help");
}
}
#[test]
fn test_test_inquire_validation_with_help() {
let test_inquire = TestInquire::new();
test_inquire.add_text("valid input");
let validator = |_: &str| -> Result<bool, String> { Ok(true) };
let result = test_inquire.text_with_validation(
"Test prompt",
Some("Helper text"),
validator,
"Not applicable",
);
assert!(result.is_ok());
if let Ok(text) = result {
assert_eq!(text, "valid input");
}
}
#[test]
fn test_test_inquire_text_explicit_ok() {
let test_inquire = TestInquire::new();
test_inquire.queue_text_response(Ok("explicit ok".to_string()));
let result = test_inquire.text("Test", None);
assert!(result.is_ok());
if let Ok(text) = result {
assert_eq!(text, "explicit ok");
}
}
#[test]
fn test_test_inquire_mixed_errors_and_successes() {
let test_inquire = TestInquire::new();
test_inquire.add_text("success");
test_inquire.queue_text_response(Err(Report::msg("First error")));
test_inquire.add_text("another success");
let result1 = test_inquire.text("", None);
assert!(result1.is_ok());
if let Ok(text) = result1 {
assert_eq!(text, "success");
}
let err_result = test_inquire.text("", None);
assert!(err_result.is_err());
if let Err(err) = err_result {
assert!(err.to_string().contains("First error"));
}
let result3 = test_inquire.text("", None);
assert!(result3.is_ok());
if let Ok(text) = result3 {
assert_eq!(text, "another success");
}
}
}