use std::fmt;
use std::path::PathBuf;
pub type CliResult<T> = Result<T, CliError>;
#[derive(Debug)]
pub enum CliError {
NotImplemented {
what: &'static str,
hint: &'static str,
},
NotInWorkspace { searched_from: PathBuf },
Io(std::io::Error),
Other { message: String, exit_code: u8 },
Silent { exit_code: u8 },
}
impl fmt::Display for CliError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CliError::NotImplemented { what, hint } => {
write!(f, "{what} is not implemented yet — {hint}")
}
CliError::NotInWorkspace { searched_from } => {
write!(
f,
"not inside an Aristo workspace (no aristo.toml found at or above {})\n\
hint: run `aristo init` to bootstrap a new workspace here",
searched_from.display()
)
}
CliError::Io(e) => write!(f, "io: {e}"),
CliError::Other { message, .. } => write!(f, "{message}"),
CliError::Silent { .. } => Ok(()),
}
}
}
impl CliError {
pub fn is_silent(&self) -> bool {
matches!(self, CliError::Silent { .. })
}
}
impl std::error::Error for CliError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
CliError::Io(e) => Some(e),
_ => None,
}
}
}
impl From<std::io::Error> for CliError {
fn from(e: std::io::Error) -> Self {
CliError::Io(e)
}
}
impl CliError {
pub fn exit_code(&self) -> u8 {
match self {
CliError::NotImplemented { .. } => 64,
CliError::NotInWorkspace { .. } => 2,
CliError::Io(_) => 1,
CliError::Other { exit_code, .. } => *exit_code,
CliError::Silent { exit_code } => *exit_code,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn not_implemented_message_includes_what_and_hint() {
let e = CliError::NotImplemented {
what: "aristo init",
hint: "see docs/deferred/...",
};
let msg = e.to_string();
assert!(msg.contains("aristo init"), "msg: {msg}");
assert!(msg.contains("see docs/deferred/..."), "msg: {msg}");
assert!(msg.contains("not implemented yet"), "msg: {msg}");
}
#[test]
fn not_in_workspace_includes_search_origin_and_hint() {
let e = CliError::NotInWorkspace {
searched_from: PathBuf::from("/tmp/elsewhere"),
};
let msg = e.to_string();
assert!(msg.contains("/tmp/elsewhere"));
assert!(msg.contains("aristo init"), "should hint at init: {msg}");
}
#[test]
fn exit_codes_are_distinct_per_class() {
let ni = CliError::NotImplemented {
what: "x",
hint: "y",
}
.exit_code();
let nw = CliError::NotInWorkspace {
searched_from: PathBuf::new(),
}
.exit_code();
let io = CliError::Io(std::io::Error::other("boom")).exit_code();
assert_ne!(ni, nw);
assert_ne!(nw, io);
assert_ne!(ni, io);
}
}