use thiserror::Error;
#[derive(Debug, Error)]
pub enum CliError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("WAL error: {0}")]
Wal(#[from] evorule_reactor::WalError),
#[error("Rules directory does not exist: {0}")]
RulesDirNotFound(String),
#[error("No .json files found in {0}")]
NoRulesFound(String),
#[error("Invalid payload JSON: {0}")]
InvalidPayload(String),
#[error("Fact log parse error at line {line}: {reason}")]
FactLogParse {
line: usize,
reason: String,
},
#[error("Hash chain verification failed: {0}")]
HashChain(String),
#[error("Execution completed with {count} Error fact(s); fact log written for audit (exit code 3)")]
ExecutionHadErrors {
count: usize,
},
#[error("{0}")]
Other(String),
}
impl CliError {
pub fn other(msg: impl Into<String>) -> Self {
Self::Other(msg.into())
}
}
impl CliError {
pub fn exit_code(&self) -> i32 {
match self {
CliError::RulesDirNotFound(_) | CliError::NoRulesFound(_) => 2,
CliError::ExecutionHadErrors { .. } => 3,
_ => 1,
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn test_exit_code_mapping() {
assert_eq!(CliError::RulesDirNotFound("x".into()).exit_code(), 2);
assert_eq!(CliError::NoRulesFound("x".into()).exit_code(), 2);
assert_eq!(CliError::ExecutionHadErrors { count: 1 }.exit_code(), 3);
assert_eq!(CliError::Other("x".into()).exit_code(), 1);
assert_eq!(
CliError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "x")).exit_code(),
1
);
}
#[test]
fn test_from_io_error() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
let cli_err: CliError = io_err.into();
assert!(matches!(cli_err, CliError::Io(_)));
}
#[test]
fn test_display_includes_context() {
let err = CliError::FactLogParse {
line: 42,
reason: "missing type field".into(),
};
let msg = format!("{}", err);
assert!(msg.contains("42"));
assert!(msg.contains("missing type field"));
}
}