organizational_intelligence_plugin/
error.rs1use thiserror::Error;
7
8#[derive(Error, Debug)]
10pub enum OipError {
11 #[error("No data available: {context}")]
13 NoData { context: String },
14
15 #[error("Invalid data format: {message}")]
16 InvalidData { message: String },
17
18 #[error("Data validation failed: {field} - {reason}")]
19 ValidationError { field: String, reason: String },
20
21 #[error("GitHub API error: {message}")]
23 GitHubError { message: String },
24
25 #[error("Repository not found: {repo}")]
26 RepoNotFound { repo: String },
27
28 #[error("Git operation failed: {operation} - {reason}")]
29 GitError { operation: String, reason: String },
30
31 #[error("Authentication required: {message}")]
32 AuthRequired { message: String },
33
34 #[error("Model not trained: call train() before predict()")]
36 ModelNotTrained,
37
38 #[error("Insufficient data for {operation}: need {required}, got {actual}")]
39 InsufficientData {
40 operation: String,
41 required: usize,
42 actual: usize,
43 },
44
45 #[error("Computation failed: {operation} - {reason}")]
46 ComputeError { operation: String, reason: String },
47
48 #[error("GPU not available: {reason}")]
49 GpuUnavailable { reason: String },
50
51 #[error("Storage error: {operation} - {reason}")]
53 StorageError { operation: String, reason: String },
54
55 #[error("File not found: {path}")]
56 FileNotFound { path: String },
57
58 #[error("IO error: {context}")]
59 IoError {
60 context: String,
61 #[source]
62 source: std::io::Error,
63 },
64
65 #[error("Configuration error: {message}")]
67 ConfigError { message: String },
68
69 #[error("Invalid argument: {arg} - {reason}")]
70 InvalidArgument { arg: String, reason: String },
71
72 #[error("Operation failed: {message}")]
74 OperationFailed { message: String },
75
76 #[error(transparent)]
77 Other(#[from] anyhow::Error),
78}
79
80impl OipError {
81 pub fn no_data(context: impl Into<String>) -> Self {
84 Self::NoData {
85 context: context.into(),
86 }
87 }
88
89 pub fn invalid_data(message: impl Into<String>) -> Self {
90 Self::InvalidData {
91 message: message.into(),
92 }
93 }
94
95 pub fn validation(field: impl Into<String>, reason: impl Into<String>) -> Self {
96 Self::ValidationError {
97 field: field.into(),
98 reason: reason.into(),
99 }
100 }
101
102 pub fn github(message: impl Into<String>) -> Self {
103 Self::GitHubError {
104 message: message.into(),
105 }
106 }
107
108 pub fn repo_not_found(repo: impl Into<String>) -> Self {
109 Self::RepoNotFound { repo: repo.into() }
110 }
111
112 pub fn git(operation: impl Into<String>, reason: impl Into<String>) -> Self {
113 Self::GitError {
114 operation: operation.into(),
115 reason: reason.into(),
116 }
117 }
118
119 pub fn auth_required(message: impl Into<String>) -> Self {
120 Self::AuthRequired {
121 message: message.into(),
122 }
123 }
124
125 pub fn insufficient_data(operation: impl Into<String>, required: usize, actual: usize) -> Self {
126 Self::InsufficientData {
127 operation: operation.into(),
128 required,
129 actual,
130 }
131 }
132
133 pub fn compute(operation: impl Into<String>, reason: impl Into<String>) -> Self {
134 Self::ComputeError {
135 operation: operation.into(),
136 reason: reason.into(),
137 }
138 }
139
140 pub fn gpu_unavailable(reason: impl Into<String>) -> Self {
141 Self::GpuUnavailable {
142 reason: reason.into(),
143 }
144 }
145
146 pub fn storage(operation: impl Into<String>, reason: impl Into<String>) -> Self {
147 Self::StorageError {
148 operation: operation.into(),
149 reason: reason.into(),
150 }
151 }
152
153 pub fn file_not_found(path: impl Into<String>) -> Self {
154 Self::FileNotFound { path: path.into() }
155 }
156
157 pub fn io(context: impl Into<String>, source: std::io::Error) -> Self {
158 Self::IoError {
159 context: context.into(),
160 source,
161 }
162 }
163
164 pub fn config(message: impl Into<String>) -> Self {
165 Self::ConfigError {
166 message: message.into(),
167 }
168 }
169
170 pub fn invalid_arg(arg: impl Into<String>, reason: impl Into<String>) -> Self {
171 Self::InvalidArgument {
172 arg: arg.into(),
173 reason: reason.into(),
174 }
175 }
176
177 pub fn failed(message: impl Into<String>) -> Self {
178 Self::OperationFailed {
179 message: message.into(),
180 }
181 }
182
183 pub fn recovery_hint(&self) -> Option<&'static str> {
187 match self {
188 Self::NoData { .. } => Some("Try analyzing a repository first with 'oip-gpu analyze'"),
189 Self::RepoNotFound { .. } => {
190 Some("Check the repository name format (owner/repo) and ensure it exists")
191 }
192 Self::AuthRequired { .. } => Some("Set GITHUB_TOKEN environment variable"),
193 Self::ModelNotTrained => Some("Train the model first with predictor.train(features)"),
194 Self::InsufficientData { .. } => Some("Provide more training data or reduce k value"),
195 Self::GpuUnavailable { .. } => {
196 Some("Use --backend simd for CPU fallback, or install GPU drivers")
197 }
198 Self::FileNotFound { .. } => Some("Check the file path and ensure it exists"),
199 Self::ConfigError { .. } => Some("Check configuration file syntax (YAML/TOML)"),
200 Self::InvalidArgument { .. } => Some("Run with --help to see valid arguments"),
201 _ => None,
202 }
203 }
204
205 pub fn is_recoverable(&self) -> bool {
207 matches!(
208 self,
209 Self::NoData { .. }
210 | Self::RepoNotFound { .. }
211 | Self::AuthRequired { .. }
212 | Self::ModelNotTrained
213 | Self::InsufficientData { .. }
214 | Self::GpuUnavailable { .. }
215 | Self::FileNotFound { .. }
216 | Self::ConfigError { .. }
217 | Self::InvalidArgument { .. }
218 )
219 }
220
221 pub fn category(&self) -> &'static str {
223 match self {
224 Self::NoData { .. } | Self::InvalidData { .. } | Self::ValidationError { .. } => "data",
225 Self::GitHubError { .. }
226 | Self::RepoNotFound { .. }
227 | Self::GitError { .. }
228 | Self::AuthRequired { .. } => "git",
229 Self::ModelNotTrained
230 | Self::InsufficientData { .. }
231 | Self::ComputeError { .. }
232 | Self::GpuUnavailable { .. } => "compute",
233 Self::StorageError { .. } | Self::FileNotFound { .. } | Self::IoError { .. } => {
234 "storage"
235 }
236 Self::ConfigError { .. } | Self::InvalidArgument { .. } => "config",
237 Self::OperationFailed { .. } | Self::Other(_) => "other",
238 }
239 }
240}
241
242pub type OipResult<T> = Result<T, OipError>;
244
245pub trait ResultExt<T> {
247 fn context(self, context: impl Into<String>) -> OipResult<T>;
249
250 fn with_context<F, S>(self, f: F) -> OipResult<T>
252 where
253 F: FnOnce() -> S,
254 S: Into<String>;
255}
256
257impl<T, E: Into<OipError>> ResultExt<T> for Result<T, E> {
258 fn context(self, context: impl Into<String>) -> OipResult<T> {
259 self.map_err(|e| {
260 let inner = e.into();
261 OipError::OperationFailed {
262 message: format!("{}: {}", context.into(), inner),
263 }
264 })
265 }
266
267 fn with_context<F, S>(self, f: F) -> OipResult<T>
268 where
269 F: FnOnce() -> S,
270 S: Into<String>,
271 {
272 self.map_err(|e| {
273 let inner = e.into();
274 OipError::OperationFailed {
275 message: format!("{}: {}", f().into(), inner),
276 }
277 })
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn test_error_display() {
287 let err = OipError::no_data("empty feature store");
288 assert!(err.to_string().contains("No data available"));
289 assert!(err.to_string().contains("empty feature store"));
290 }
291
292 #[test]
293 fn test_error_recovery_hint() {
294 let err = OipError::ModelNotTrained;
295 assert!(err.recovery_hint().is_some());
296 assert!(err.recovery_hint().unwrap().contains("train"));
297 }
298
299 #[test]
300 fn test_error_is_recoverable() {
301 assert!(OipError::ModelNotTrained.is_recoverable());
302 assert!(OipError::repo_not_found("test/repo").is_recoverable());
303 assert!(!OipError::failed("unknown").is_recoverable());
304 }
305
306 #[test]
307 fn test_error_category() {
308 assert_eq!(OipError::ModelNotTrained.category(), "compute");
309 assert_eq!(OipError::repo_not_found("test").category(), "git");
310 assert_eq!(OipError::no_data("test").category(), "data");
311 }
312
313 #[test]
314 fn test_insufficient_data_error() {
315 let err = OipError::insufficient_data("k-means clustering", 10, 5);
316 assert!(err.to_string().contains("10"));
317 assert!(err.to_string().contains("5"));
318 assert!(err.is_recoverable());
319 }
320
321 #[test]
322 fn test_validation_error() {
323 let err = OipError::validation("category", "must be 0-9");
324 assert!(err.to_string().contains("category"));
325 assert!(err.to_string().contains("must be 0-9"));
326 }
327
328 #[test]
329 fn test_result_context() {
330 let result: Result<(), OipError> = Err(OipError::no_data("test"));
331 let with_context = result.context("during analysis");
332 assert!(with_context.is_err());
333 assert!(with_context.unwrap_err().to_string().contains("analysis"));
334 }
335
336 #[test]
337 fn test_result_with_context() {
338 let result: Result<(), OipError> = Err(OipError::no_data("test"));
339 let with_context = result.with_context(|| "lazy context");
340 assert!(with_context.is_err());
341 assert!(with_context.unwrap_err().to_string().contains("lazy"));
342 }
343
344 #[test]
345 fn test_invalid_data_constructor() {
346 let err = OipError::invalid_data("malformed JSON");
347 assert!(err.to_string().contains("Invalid data format"));
348 assert_eq!(err.category(), "data");
349 }
350
351 #[test]
352 fn test_github_error_constructor() {
353 let err = OipError::github("rate limit exceeded");
354 assert!(err.to_string().contains("GitHub API error"));
355 assert_eq!(err.category(), "git");
356 }
357
358 #[test]
359 fn test_git_error_constructor() {
360 let err = OipError::git("clone", "network timeout");
361 assert!(err.to_string().contains("Git operation failed"));
362 assert!(err.to_string().contains("clone"));
363 assert_eq!(err.category(), "git");
364 }
365
366 #[test]
367 fn test_auth_required_constructor() {
368 let err = OipError::auth_required("GitHub API requires token");
369 assert!(err.to_string().contains("Authentication required"));
370 assert!(err.recovery_hint().is_some());
371 assert!(err.recovery_hint().unwrap().contains("GITHUB_TOKEN"));
372 assert!(err.is_recoverable());
373 }
374
375 #[test]
376 fn test_compute_error_constructor() {
377 let err = OipError::compute("correlation", "division by zero");
378 assert!(err.to_string().contains("Computation failed"));
379 assert_eq!(err.category(), "compute");
380 }
381
382 #[test]
383 fn test_gpu_unavailable_constructor() {
384 let err = OipError::gpu_unavailable("no Vulkan driver");
385 assert!(err.to_string().contains("GPU not available"));
386 assert!(err.recovery_hint().unwrap().contains("simd"));
387 assert!(err.is_recoverable());
388 }
389
390 #[test]
391 fn test_storage_error_constructor() {
392 let err = OipError::storage("save", "disk full");
393 assert!(err.to_string().contains("Storage error"));
394 assert_eq!(err.category(), "storage");
395 }
396
397 #[test]
398 fn test_file_not_found_constructor() {
399 let err = OipError::file_not_found("/tmp/missing.db");
400 assert!(err.to_string().contains("File not found"));
401 assert!(err.recovery_hint().is_some());
402 assert!(err.is_recoverable());
403 }
404
405 #[test]
406 fn test_io_error_constructor() {
407 let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
408 let err = OipError::io("reading file", io_err);
409 assert!(err.to_string().contains("IO error"));
410 assert_eq!(err.category(), "storage");
411 }
412
413 #[test]
414 fn test_config_error_constructor() {
415 let err = OipError::config("invalid YAML syntax");
416 assert!(err.to_string().contains("Configuration error"));
417 assert!(err.recovery_hint().unwrap().contains("YAML"));
418 assert!(err.is_recoverable());
419 }
420
421 #[test]
422 fn test_invalid_arg_constructor() {
423 let err = OipError::invalid_arg("--backend", "must be simd or gpu");
424 assert!(err.to_string().contains("Invalid argument"));
425 assert!(err.recovery_hint().unwrap().contains("--help"));
426 assert!(err.is_recoverable());
427 }
428
429 #[test]
430 fn test_failed_constructor() {
431 let err = OipError::failed("network unreachable");
432 assert!(err.to_string().contains("Operation failed"));
433 assert!(!err.is_recoverable());
434 assert_eq!(err.category(), "other");
435 }
436
437 #[test]
438 fn test_model_not_trained_recovery() {
439 let err = OipError::ModelNotTrained;
440 assert!(err.recovery_hint().unwrap().contains("train"));
441 assert!(err.is_recoverable());
442 assert_eq!(err.category(), "compute");
443 }
444
445 #[test]
446 fn test_repo_not_found_recovery() {
447 let err = OipError::repo_not_found("invalid/repo");
448 assert!(err.recovery_hint().unwrap().contains("owner/repo"));
449 assert!(err.is_recoverable());
450 }
451
452 #[test]
453 fn test_no_data_recovery() {
454 let err = OipError::no_data("empty store");
455 assert!(err.recovery_hint().unwrap().contains("analyze"));
456 assert!(err.is_recoverable());
457 }
458
459 #[test]
460 fn test_non_recoverable_errors() {
461 assert!(!OipError::invalid_data("test").is_recoverable());
462 assert!(!OipError::github("test").is_recoverable());
463 assert!(!OipError::git("op", "reason").is_recoverable());
464 assert!(!OipError::compute("op", "reason").is_recoverable());
465 assert!(!OipError::storage("op", "reason").is_recoverable());
466 }
467
468 #[test]
469 fn test_category_assignments() {
470 assert_eq!(OipError::invalid_data("test").category(), "data");
472 assert_eq!(OipError::validation("f", "r").category(), "data");
473
474 assert_eq!(OipError::github("test").category(), "git");
476 assert_eq!(OipError::git("o", "r").category(), "git");
477 assert_eq!(OipError::auth_required("test").category(), "git");
478
479 assert_eq!(OipError::compute("o", "r").category(), "compute");
481
482 assert_eq!(OipError::storage("o", "r").category(), "storage");
484 let io = std::io::Error::other("test");
485 assert_eq!(OipError::io("ctx", io).category(), "storage");
486
487 assert_eq!(OipError::config("test").category(), "config");
489 assert_eq!(OipError::invalid_arg("a", "r").category(), "config");
490 }
491}