use std::io::BufRead;
use crate::output::Streams;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Go {
Nothing,
DryRun,
Write,
Ask,
Refuse,
}
#[must_use]
pub(crate) fn decide(dry_run: bool, yes: bool, stdin_is_terminal: bool, has_changes: bool) -> Go {
if !has_changes {
return Go::Nothing;
}
if dry_run {
Go::DryRun
} else if yes {
Go::Write
} else if stdin_is_terminal {
Go::Ask
} else {
Go::Refuse
}
}
#[derive(Debug)]
pub(crate) enum Answer {
Yes,
No,
Failed(String),
}
pub(crate) fn confirm(
streams: &mut Streams<'_>,
from: &str,
to: &str,
path: &str,
changes: usize,
reader: &mut dyn BufRead,
) -> Answer {
let question = format!(
"copy {changes} change{} from `{from}` onto `{to}` at `{path}`? [y/N] ",
if changes == 1 { "" } else { "s" }
);
streams.aside("confirm", &question);
let mut answer = String::new();
match reader.read_line(&mut answer) {
Ok(0) => Answer::No,
Ok(_) => {
if matches!(answer.trim().to_ascii_lowercase().as_str(), "y" | "yes") {
Answer::Yes
} else {
Answer::No
}
}
Err(err) => Answer::Failed(err.to_string()),
}
}
#[cfg(test)]
mod tests {
use std::io::{Cursor, Error as IoError, ErrorKind};
use super::*;
use crate::cli::Format;
#[test]
fn a_confirmation_read_failure_is_not_an_answer() {
struct Unreadable;
impl std::io::Read for Unreadable {
fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
Err(IoError::new(ErrorKind::BrokenPipe, "stdin is gone"))
}
}
impl BufRead for Unreadable {
fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
Err(IoError::new(ErrorKind::BrokenPipe, "stdin is gone"))
}
fn consume(&mut self, _len: usize) {}
}
let mut out = Vec::new();
let mut err = Vec::new();
let mut streams = crate::output::Streams {
out: &mut out,
err: &mut err,
style: crate::style::Presentation::BARE,
fmt: Format::Table,
};
let mut ask = |reader: &mut dyn BufRead| {
confirm(&mut streams, "web", "api", "dogs.kelpie", 2, reader)
};
let mut broken = Unreadable;
let Answer::Failed(reason) = ask(&mut broken) else {
panic!("a failed read is not an answer");
};
assert!(reason.contains("stdin is gone"), "{reason}");
let mut yes = Cursor::new(b"y\n".as_slice());
assert!(matches!(ask(&mut yes), Answer::Yes));
let mut maybe = Cursor::new(b"maybe\n".as_slice());
assert!(matches!(ask(&mut maybe), Answer::No));
let mut silent = Cursor::new(b"".as_slice());
assert!(matches!(ask(&mut silent), Answer::No));
let asked = String::from_utf8(err).unwrap();
assert_eq!(
asked.matches("[y/N]").count(),
4,
"every one of them was asked the same question, on stderr"
);
assert!(asked.contains("copy 2 changes from `web` onto `api` at `dogs.kelpie`?"));
}
}