use std::io::Write;
use std::process::ExitCode;
pub const MALFUNCTION: u8 = 3;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Verdict {
Proceed(String),
Adjust(String),
Stop(String),
}
impl Verdict {
#[must_use]
pub fn code(&self) -> u8 {
match self {
Verdict::Proceed(_) => 0,
Verdict::Adjust(_) => 1,
Verdict::Stop(_) => 2,
}
}
#[must_use = "the exit code is the verdict; dropping it reports success"]
pub fn render(self) -> ExitCode {
ExitCode::from(self.render_to(&mut std::io::stdout()))
}
fn render_to(self, stdout: &mut impl Write) -> u8 {
let code = self.code();
let (Verdict::Proceed(text) | Verdict::Adjust(text) | Verdict::Stop(text)) = self;
if let Err(e) = writeln!(stdout, "{text}").and_then(|()| stdout.flush()) {
let _ = writeln!(
std::io::stderr(),
"fairway: the verdict could not be written to stdout: {e}"
);
return MALFUNCTION;
}
code
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exit_codes_follow_the_contract() {
assert_eq!(Verdict::Proceed(String::new()).code(), 0);
assert_eq!(Verdict::Adjust(String::new()).code(), 1);
assert_eq!(Verdict::Stop(String::new()).code(), 2);
assert_eq!(MALFUNCTION, 3);
}
#[test]
fn the_instruction_reaches_the_stream_verbatim() {
let mut stream = Vec::new();
let code = Verdict::Adjust(String::from("go another way")).render_to(&mut stream);
assert_eq!(code, 1);
assert_eq!(stream, b"go another way\n");
}
struct DeadStream;
impl Write for DeadStream {
fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
Err(std::io::Error::from(std::io::ErrorKind::BrokenPipe))
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[test]
fn a_dead_stream_is_a_malfunction() {
let code = Verdict::Proceed(String::from("carry on")).render_to(&mut DeadStream);
assert_eq!(code, MALFUNCTION);
}
}