use std::path::PathBuf;
pub const EXIT_OK: i32 = 0;
pub const EXIT_USAGE: i32 = 1;
pub const EXIT_DESIGN: i32 = 2;
pub const EXIT_IO: i32 = 3;
pub const EXIT_PROBLEM: i32 = 4;
pub const EXIT_RENDER: i32 = 5;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CliError {
pub code: i32,
pub message: String,
}
impl CliError {
pub fn usage(message: impl Into<String>) -> Self {
Self {
code: EXIT_USAGE,
message: message.into(),
}
}
pub fn design(message: impl Into<String>) -> Self {
Self {
code: EXIT_DESIGN,
message: message.into(),
}
}
pub fn io(message: impl Into<String>) -> Self {
Self {
code: EXIT_IO,
message: message.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutFile {
pub path: PathBuf,
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Outcome {
pub stdout: String,
pub stderr: String,
pub files: Vec<OutFile>,
pub exit: i32,
}
impl Outcome {
#[must_use]
pub fn text(stdout: String) -> Self {
Self {
stdout,
..Self::default()
}
}
#[must_use]
pub fn failure(error: &CliError) -> Self {
Self {
stderr: format!("error: {}\n", error.message),
exit: error.code,
..Self::default()
}
}
#[must_use]
pub fn with_file(mut self, path: PathBuf, text: String) -> Self {
self.files.push(OutFile { path, text });
self
}
#[must_use]
pub fn with_note(mut self, note: &str) -> Self {
self.stderr.push_str(note);
self.stderr.push('\n');
self
}
#[must_use]
pub const fn with_exit(mut self, exit: i32) -> Self {
self.exit = exit;
self
}
}
pub type CommandResult = Result<Outcome, CliError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn errors_carry_their_exit_codes() {
assert_eq!(CliError::usage("x").code, EXIT_USAGE);
assert_eq!(CliError::design("x").code, EXIT_DESIGN);
assert_eq!(CliError::io("x").code, EXIT_IO);
}
#[test]
fn the_exit_codes_are_distinct() {
let codes = [
EXIT_OK,
EXIT_USAGE,
EXIT_DESIGN,
EXIT_IO,
EXIT_PROBLEM,
EXIT_RENDER,
];
for (index, code) in codes.iter().enumerate() {
assert_eq!(
codes.iter().filter(|other| *other == code).count(),
1,
"{index}"
);
}
assert_eq!(EXIT_RENDER, 5);
}
#[test]
fn a_failure_is_one_error_line_and_its_code() {
let outcome = Outcome::failure(&CliError::design("does not solve"));
assert_eq!(outcome.stderr, "error: does not solve\n");
assert_eq!(outcome.exit, EXIT_DESIGN);
assert_eq!(outcome.stdout, "");
assert_eq!(outcome.files.len(), 0);
}
#[test]
fn builders_keep_order() {
let outcome = Outcome::text("a\n".to_string())
.with_file(PathBuf::from("one"), "1".to_string())
.with_file(PathBuf::from("two"), "2".to_string())
.with_note("first")
.with_note("second")
.with_exit(EXIT_PROBLEM);
assert_eq!(outcome.files[0].path, PathBuf::from("one"));
assert_eq!(outcome.files[1].path, PathBuf::from("two"));
assert_eq!(outcome.stderr, "first\nsecond\n");
assert_eq!(outcome.exit, EXIT_PROBLEM);
}
}