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 #[error("HTTP 404 Not Found: {0}")]
60 HttpNotFound(String),
61}
62
63impl CliError {
64 pub fn exit_code(&self) -> ExitCode {
66 match self {
67 Self::FileNotFound(_) | Self::NotAFile(_) => ExitCode::from(3),
68 Self::InvalidFormat(_) => ExitCode::from(4),
69 Self::Io(_) => ExitCode::from(7),
70 Self::ValidationFailed(_) => ExitCode::from(5),
71 Self::Aprender(_) => ExitCode::from(1),
72 Self::ModelLoadFailed(_) => ExitCode::from(6),
73 Self::InferenceFailed(_) => ExitCode::from(8),
74 Self::FeatureDisabled(_) => ExitCode::from(9),
75 Self::NetworkError(_) => ExitCode::from(10),
76 Self::HttpNotFound(_) => ExitCode::from(11),
77 }
78 }
79}
80
81impl From<aprender::error::AprenderError> for CliError {
82 fn from(e: aprender::error::AprenderError) -> Self {
83 Self::Aprender(e.to_string())
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use std::path::PathBuf;
91
92 #[test]
95 fn test_file_not_found_exit_code() {
96 let err = CliError::FileNotFound(PathBuf::from("/test"));
97 assert_eq!(err.exit_code(), ExitCode::from(3));
98 }
99
100 #[test]
101 fn test_not_a_file_exit_code() {
102 let err = CliError::NotAFile(PathBuf::from("/test"));
103 assert_eq!(err.exit_code(), ExitCode::from(3));
104 }
105
106 #[test]
107 fn test_invalid_format_exit_code() {
108 let err = CliError::InvalidFormat("bad".to_string());
109 assert_eq!(err.exit_code(), ExitCode::from(4));
110 }
111
112 #[test]
113 fn test_io_error_exit_code() {
114 let err = CliError::Io(std::io::Error::new(std::io::ErrorKind::Other, "test"));
115 assert_eq!(err.exit_code(), ExitCode::from(7));
116 }
117
118 #[test]
119 fn test_validation_failed_exit_code() {
120 let err = CliError::ValidationFailed("test".to_string());
121 assert_eq!(err.exit_code(), ExitCode::from(5));
122 }
123
124 #[test]
125 fn test_aprender_error_exit_code() {
126 let err = CliError::Aprender("test".to_string());
127 assert_eq!(err.exit_code(), ExitCode::from(1));
128 }
129
130 #[test]
131 fn test_model_load_failed_exit_code() {
132 let err = CliError::ModelLoadFailed("test".to_string());
133 assert_eq!(err.exit_code(), ExitCode::from(6));
134 }
135
136 #[test]
137 fn test_inference_failed_exit_code() {
138 let err = CliError::InferenceFailed("test".to_string());
139 assert_eq!(err.exit_code(), ExitCode::from(8));
140 }
141
142 #[test]
143 fn test_feature_disabled_exit_code() {
144 let err = CliError::FeatureDisabled("test".to_string());
145 assert_eq!(err.exit_code(), ExitCode::from(9));
146 }
147
148 #[test]
149 fn test_network_error_exit_code() {
150 let err = CliError::NetworkError("test".to_string());
151 assert_eq!(err.exit_code(), ExitCode::from(10));
152 }
153
154 #[test]
155 fn test_http_not_found_exit_code() {
156 let err = CliError::HttpNotFound("test".to_string());
157 assert_eq!(err.exit_code(), ExitCode::from(11));
158 }
159
160 #[test]
163 fn test_file_not_found_display() {
164 let err = CliError::FileNotFound(PathBuf::from("/model.apr"));
165 assert_eq!(err.to_string(), "File not found: /model.apr");
166 }
167
168 #[test]
169 fn test_not_a_file_display() {
170 let err = CliError::NotAFile(PathBuf::from("/dir"));
171 assert_eq!(err.to_string(), "Not a file: /dir");
172 }
173
174 #[test]
175 fn test_invalid_format_display() {
176 let err = CliError::InvalidFormat("bad magic".to_string());
177 assert_eq!(err.to_string(), "Invalid APR format: bad magic");
178 }
179
180 #[test]
181 fn test_validation_failed_display() {
182 let err = CliError::ValidationFailed("missing field".to_string());
183 assert_eq!(err.to_string(), "Validation failed: missing field");
184 }
185
186 #[test]
187 fn test_aprender_error_display() {
188 let err = CliError::Aprender("internal".to_string());
189 assert_eq!(err.to_string(), "Aprender error: internal");
190 }
191
192 #[test]
193 fn test_model_load_failed_display() {
194 let err = CliError::ModelLoadFailed("corrupt".to_string());
195 assert_eq!(err.to_string(), "Model load failed: corrupt");
196 }
197
198 #[test]
199 fn test_inference_failed_display() {
200 let err = CliError::InferenceFailed("OOM".to_string());
201 assert_eq!(err.to_string(), "Inference failed: OOM");
202 }
203
204 #[test]
205 fn test_feature_disabled_display() {
206 let err = CliError::FeatureDisabled("cuda".to_string());
207 assert_eq!(err.to_string(), "Feature not enabled: cuda");
208 }
209
210 #[test]
211 fn test_network_error_display() {
212 let err = CliError::NetworkError("timeout".to_string());
213 assert_eq!(err.to_string(), "Network error: timeout");
214 }
215
216 #[test]
217 fn test_http_not_found_display() {
218 let err = CliError::HttpNotFound("tokenizer.json".to_string());
219 assert_eq!(err.to_string(), "HTTP 404 Not Found: tokenizer.json");
220 }
221
222 #[test]
225 fn test_io_error_conversion() {
226 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
227 let cli_err: CliError = io_err.into();
228 assert!(cli_err.to_string().contains("file missing"));
229 assert_eq!(cli_err.exit_code(), ExitCode::from(7));
230 }
231
232 #[test]
233 fn test_debug_impl() {
234 let err = CliError::FileNotFound(PathBuf::from("/test"));
235 let debug = format!("{:?}", err);
236 assert!(debug.contains("FileNotFound"));
237 }
238
239 #[test]
242 fn test_result_type_ok() {
243 let result: Result<i32> = Ok(42);
244 assert_eq!(result.unwrap(), 42);
245 }
246
247 #[test]
248 fn test_result_type_err() {
249 let result: Result<i32> = Err(CliError::InvalidFormat("test".to_string()));
250 assert!(result.is_err());
251 }
252
253 #[test]
256 fn test_all_exit_codes_are_distinct_per_category() {
257 let codes = vec![
259 (
260 CliError::FileNotFound(PathBuf::from("a")).exit_code(),
261 "file",
262 ),
263 (
264 CliError::InvalidFormat("a".to_string()).exit_code(),
265 "format",
266 ),
267 (
268 CliError::Io(std::io::Error::new(std::io::ErrorKind::Other, "")).exit_code(),
269 "io",
270 ),
271 (
272 CliError::ValidationFailed("a".to_string()).exit_code(),
273 "validation",
274 ),
275 (CliError::Aprender("a".to_string()).exit_code(), "aprender"),
276 (
277 CliError::ModelLoadFailed("a".to_string()).exit_code(),
278 "model_load",
279 ),
280 (
281 CliError::InferenceFailed("a".to_string()).exit_code(),
282 "inference",
283 ),
284 (
285 CliError::FeatureDisabled("a".to_string()).exit_code(),
286 "feature",
287 ),
288 (
289 CliError::NetworkError("a".to_string()).exit_code(),
290 "network",
291 ),
292 (
293 CliError::HttpNotFound("a".to_string()).exit_code(),
294 "http_not_found",
295 ),
296 ];
297 assert_eq!(codes[0].0, ExitCode::from(3));
299 }
300}