1use thiserror::Error;
11
12#[derive(Debug, Error)]
19pub enum CliError {
20 #[error("I/O error: {0}")]
22 Io(#[from] std::io::Error),
23
24 #[error("JSON error: {0}")]
26 Json(#[from] serde_json::Error),
27
28 #[error("WAL error: {0}")]
30 Wal(#[from] evorule_reactor::WalError),
31
32 #[error("Rules directory does not exist: {0}")]
34 RulesDirNotFound(String),
35
36 #[error("No .json files found in {0}")]
38 NoRulesFound(String),
39
40 #[error("Invalid payload JSON: {0}")]
42 InvalidPayload(String),
43
44 #[error("Fact log parse error at line {line}: {reason}")]
46 FactLogParse {
47 line: usize,
49 reason: String,
51 },
52
53 #[error("Hash chain verification failed: {0}")]
55 HashChain(String),
56
57 #[error("{0}")]
59 Other(String),
60}
61
62impl CliError {
63 pub fn other(msg: impl Into<String>) -> Self {
73 Self::Other(msg.into())
74 }
75}
76
77impl CliError {
84 pub fn exit_code(&self) -> i32 {
103 match self {
104 CliError::RulesDirNotFound(_) | CliError::NoRulesFound(_) => 2,
105 _ => 1,
106 }
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 #![allow(clippy::unwrap_used)]
113 use super::*;
114
115 #[test]
116 fn test_exit_code_mapping() {
117 assert_eq!(CliError::RulesDirNotFound("x".into()).exit_code(), 2);
118 assert_eq!(CliError::NoRulesFound("x".into()).exit_code(), 2);
119 assert_eq!(CliError::Other("x".into()).exit_code(), 1);
120 assert_eq!(
121 CliError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "x")).exit_code(),
122 1
123 );
124 }
125
126 #[test]
127 fn test_from_io_error() {
128 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
129 let cli_err: CliError = io_err.into();
130 assert!(matches!(cli_err, CliError::Io(_)));
131 }
132
133 #[test]
134 fn test_display_includes_context() {
135 let err = CliError::FactLogParse {
136 line: 42,
137 reason: "missing type field".into(),
138 };
139 let msg = format!("{}", err);
140 assert!(msg.contains("42"));
141 assert!(msg.contains("missing type field"));
142 }
143}