1use std::path::PathBuf;
6use std::process::ExitCode;
7use thiserror::Error;
8
9pub type Result<T> = std::result::Result<T, CliError>;
11
12#[derive(Error, Debug)]
14pub enum CliError {
15 #[error("File not found: {0}")]
17 FileNotFound(PathBuf),
18
19 #[error("Not a file: {0}")]
21 NotAFile(PathBuf),
22
23 #[error("Invalid APR format: {0}")]
25 InvalidFormat(String),
26
27 #[error("IO error: {0}")]
29 Io(#[from] std::io::Error),
30
31 #[error("Validation failed: {0}")]
33 ValidationFailed(String),
34
35 #[error("Aprender error: {0}")]
37 Aprender(String),
38
39 #[error("Model load failed: {0}")]
41 #[allow(dead_code)]
42 ModelLoadFailed(String),
43
44 #[error("Inference failed: {0}")]
46 #[allow(dead_code)]
47 InferenceFailed(String),
48
49 #[error("Feature not enabled: {0}")]
51 #[allow(dead_code)]
52 FeatureDisabled(String),
53
54 #[error("Network error: {0}")]
56 NetworkError(String),
57}
58
59impl CliError {
60 pub fn exit_code(&self) -> ExitCode {
62 match self {
63 Self::FileNotFound(_) | Self::NotAFile(_) => ExitCode::from(3),
64 Self::InvalidFormat(_) => ExitCode::from(4),
65 Self::Io(_) => ExitCode::from(7),
66 Self::ValidationFailed(_) => ExitCode::from(5),
67 Self::Aprender(_) => ExitCode::from(1),
68 Self::ModelLoadFailed(_) => ExitCode::from(6),
69 Self::InferenceFailed(_) => ExitCode::from(8),
70 Self::FeatureDisabled(_) => ExitCode::from(9),
71 Self::NetworkError(_) => ExitCode::from(10),
72 }
73 }
74}
75
76impl From<aprender::error::AprenderError> for CliError {
77 fn from(e: aprender::error::AprenderError) -> Self {
78 Self::Aprender(e.to_string())
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use std::path::PathBuf;
86
87 #[test]
90 fn test_file_not_found_exit_code() {
91 let err = CliError::FileNotFound(PathBuf::from("/test"));
92 assert_eq!(err.exit_code(), ExitCode::from(3));
93 }
94
95 #[test]
96 fn test_not_a_file_exit_code() {
97 let err = CliError::NotAFile(PathBuf::from("/test"));
98 assert_eq!(err.exit_code(), ExitCode::from(3));
99 }
100
101 #[test]
102 fn test_invalid_format_exit_code() {
103 let err = CliError::InvalidFormat("bad".to_string());
104 assert_eq!(err.exit_code(), ExitCode::from(4));
105 }
106
107 #[test]
108 fn test_io_error_exit_code() {
109 let err = CliError::Io(std::io::Error::new(std::io::ErrorKind::Other, "test"));
110 assert_eq!(err.exit_code(), ExitCode::from(7));
111 }
112
113 #[test]
114 fn test_validation_failed_exit_code() {
115 let err = CliError::ValidationFailed("test".to_string());
116 assert_eq!(err.exit_code(), ExitCode::from(5));
117 }
118
119 #[test]
120 fn test_aprender_error_exit_code() {
121 let err = CliError::Aprender("test".to_string());
122 assert_eq!(err.exit_code(), ExitCode::from(1));
123 }
124
125 #[test]
126 fn test_model_load_failed_exit_code() {
127 let err = CliError::ModelLoadFailed("test".to_string());
128 assert_eq!(err.exit_code(), ExitCode::from(6));
129 }
130
131 #[test]
132 fn test_inference_failed_exit_code() {
133 let err = CliError::InferenceFailed("test".to_string());
134 assert_eq!(err.exit_code(), ExitCode::from(8));
135 }
136
137 #[test]
138 fn test_feature_disabled_exit_code() {
139 let err = CliError::FeatureDisabled("test".to_string());
140 assert_eq!(err.exit_code(), ExitCode::from(9));
141 }
142
143 #[test]
144 fn test_network_error_exit_code() {
145 let err = CliError::NetworkError("test".to_string());
146 assert_eq!(err.exit_code(), ExitCode::from(10));
147 }
148
149 #[test]
152 fn test_file_not_found_display() {
153 let err = CliError::FileNotFound(PathBuf::from("/model.apr"));
154 assert_eq!(err.to_string(), "File not found: /model.apr");
155 }
156
157 #[test]
158 fn test_not_a_file_display() {
159 let err = CliError::NotAFile(PathBuf::from("/dir"));
160 assert_eq!(err.to_string(), "Not a file: /dir");
161 }
162
163 #[test]
164 fn test_invalid_format_display() {
165 let err = CliError::InvalidFormat("bad magic".to_string());
166 assert_eq!(err.to_string(), "Invalid APR format: bad magic");
167 }
168
169 #[test]
170 fn test_validation_failed_display() {
171 let err = CliError::ValidationFailed("missing field".to_string());
172 assert_eq!(err.to_string(), "Validation failed: missing field");
173 }
174
175 #[test]
176 fn test_aprender_error_display() {
177 let err = CliError::Aprender("internal".to_string());
178 assert_eq!(err.to_string(), "Aprender error: internal");
179 }
180
181 #[test]
182 fn test_model_load_failed_display() {
183 let err = CliError::ModelLoadFailed("corrupt".to_string());
184 assert_eq!(err.to_string(), "Model load failed: corrupt");
185 }
186
187 #[test]
188 fn test_inference_failed_display() {
189 let err = CliError::InferenceFailed("OOM".to_string());
190 assert_eq!(err.to_string(), "Inference failed: OOM");
191 }
192
193 #[test]
194 fn test_feature_disabled_display() {
195 let err = CliError::FeatureDisabled("cuda".to_string());
196 assert_eq!(err.to_string(), "Feature not enabled: cuda");
197 }
198
199 #[test]
200 fn test_network_error_display() {
201 let err = CliError::NetworkError("timeout".to_string());
202 assert_eq!(err.to_string(), "Network error: timeout");
203 }
204
205 #[test]
208 fn test_io_error_conversion() {
209 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
210 let cli_err: CliError = io_err.into();
211 assert!(cli_err.to_string().contains("file missing"));
212 assert_eq!(cli_err.exit_code(), ExitCode::from(7));
213 }
214
215 #[test]
216 fn test_debug_impl() {
217 let err = CliError::FileNotFound(PathBuf::from("/test"));
218 let debug = format!("{:?}", err);
219 assert!(debug.contains("FileNotFound"));
220 }
221
222 #[test]
225 fn test_result_type_ok() {
226 let result: Result<i32> = Ok(42);
227 assert_eq!(result.unwrap(), 42);
228 }
229
230 #[test]
231 fn test_result_type_err() {
232 let result: Result<i32> = Err(CliError::InvalidFormat("test".to_string()));
233 assert!(result.is_err());
234 }
235
236 #[test]
239 fn test_all_exit_codes_are_distinct_per_category() {
240 let codes = vec![
242 (
243 CliError::FileNotFound(PathBuf::from("a")).exit_code(),
244 "file",
245 ),
246 (
247 CliError::InvalidFormat("a".to_string()).exit_code(),
248 "format",
249 ),
250 (
251 CliError::Io(std::io::Error::new(std::io::ErrorKind::Other, "")).exit_code(),
252 "io",
253 ),
254 (
255 CliError::ValidationFailed("a".to_string()).exit_code(),
256 "validation",
257 ),
258 (CliError::Aprender("a".to_string()).exit_code(), "aprender"),
259 (
260 CliError::ModelLoadFailed("a".to_string()).exit_code(),
261 "model_load",
262 ),
263 (
264 CliError::InferenceFailed("a".to_string()).exit_code(),
265 "inference",
266 ),
267 (
268 CliError::FeatureDisabled("a".to_string()).exit_code(),
269 "feature",
270 ),
271 (
272 CliError::NetworkError("a".to_string()).exit_code(),
273 "network",
274 ),
275 ];
276 assert_eq!(codes[0].0, ExitCode::from(3));
278 }
279}