Skip to main content

valknut_rs/core/
errors.rs

1//! Error types for the valknut-rs library.
2//!
3//! This module provides comprehensive error handling for all valknut operations,
4//! with structured error types that preserve context and enable proper error
5//! propagation throughout the analysis pipeline.
6
7use std::io;
8use std::num::{ParseFloatError, ParseIntError};
9use std::str::Utf8Error;
10
11use thiserror::Error;
12
13/// Main result type for valknut operations.
14pub type Result<T> = std::result::Result<T, ValknutError>;
15
16/// Comprehensive error type for all valknut operations.
17#[derive(Error, Debug)]
18pub enum ValknutError {
19    /// I/O related errors (file operations, network, etc.)
20    #[error("I/O error: {message}")]
21    Io {
22        /// Human-readable error message
23        message: String,
24        /// Underlying I/O error
25        #[source]
26        source: io::Error,
27    },
28
29    /// Configuration errors
30    #[error("Configuration error: {message}")]
31    Config {
32        /// Error description
33        message: String,
34        /// Configuration field that caused the error
35        field: Option<String>,
36    },
37
38    /// Parsing and language processing errors
39    #[error("Parse error in {language}: {message}")]
40    Parse {
41        /// Programming language being parsed
42        language: String,
43        /// Error description
44        message: String,
45        /// File path where error occurred
46        file_path: Option<String>,
47        /// Line number (if available)
48        line: Option<usize>,
49        /// Column number (if available)
50        column: Option<usize>,
51    },
52
53    /// Mathematical computation errors
54    #[error("Mathematical error: {message}")]
55    Math {
56        /// Error description
57        message: String,
58        /// Context of the mathematical operation
59        context: Option<String>,
60    },
61
62    /// Graph algorithm errors
63    #[error("Graph analysis error: {message}")]
64    Graph {
65        /// Error description
66        message: String,
67        /// Graph node or edge that caused the error
68        element: Option<String>,
69    },
70
71    /// LSH and similarity detection errors
72    #[error("LSH error: {message}")]
73    Lsh {
74        /// Error description
75        message: String,
76        /// LSH parameters that may have caused the issue
77        parameters: Option<String>,
78    },
79
80    /// Analysis pipeline errors
81    #[error("Pipeline error at stage '{stage}': {message}")]
82    Pipeline {
83        /// Pipeline stage where error occurred
84        stage: String,
85        /// Error description
86        message: String,
87        /// Number of files processed before error
88        processed_count: Option<usize>,
89    },
90
91    /// Cache and storage errors
92    #[error("Cache error: {message}")]
93    Cache {
94        /// Error description
95        message: String,
96        /// Cache key that caused the issue
97        key: Option<String>,
98    },
99
100    /// Serialization/deserialization errors
101    #[error("Serialization error: {message}")]
102    Serialization {
103        /// Error description
104        message: String,
105        /// Data type being serialized
106        data_type: Option<String>,
107        /// Underlying serialization error
108        #[source]
109        source: Option<Box<dyn std::error::Error + Send + Sync>>,
110    },
111
112    /// Validation errors for input data
113    #[error("Validation error: {message}")]
114    Validation {
115        /// Error description
116        message: String,
117        /// Field or input that failed validation
118        field: Option<String>,
119        /// Expected value or format
120        expected: Option<String>,
121        /// Actual value received
122        actual: Option<String>,
123    },
124
125    /// Resource exhaustion errors
126    #[error("Resource exhaustion: {message}")]
127    ResourceExhaustion {
128        /// Error description
129        message: String,
130        /// Type of resource exhausted
131        resource_type: String,
132        /// Current usage level
133        current_usage: Option<String>,
134        /// Maximum allowed usage
135        limit: Option<String>,
136    },
137
138    /// Concurrency and threading errors
139    #[error("Concurrency error: {message}")]
140    Concurrency {
141        /// Error description
142        message: String,
143        /// Thread or task identifier
144        thread_id: Option<String>,
145    },
146
147    /// Feature not implemented or not available
148    #[error("Feature not available: {feature}")]
149    FeatureUnavailable {
150        /// Feature name
151        feature: String,
152        /// Reason why it's unavailable
153        reason: Option<String>,
154    },
155
156    /// Generic internal errors
157    #[error("Internal error: {message}")]
158    Internal {
159        /// Error description
160        message: String,
161        /// Additional context
162        context: Option<String>,
163    },
164
165    /// Unsupported operation or feature
166    #[error("Unsupported: {message}")]
167    Unsupported {
168        /// Error description
169        message: String,
170    },
171}
172
173/// Factory methods and context utilities for [`ValknutError`].
174impl ValknutError {
175    /// Create a new I/O error with context
176    pub fn io(message: impl Into<String>, source: io::Error) -> Self {
177        Self::Io {
178            message: message.into(),
179            source,
180        }
181    }
182
183    /// Create a new configuration error
184    pub fn config(message: impl Into<String>) -> Self {
185        Self::Config {
186            message: message.into(),
187            field: None,
188        }
189    }
190
191    /// Create a new configuration error with field context
192    pub fn config_field(message: impl Into<String>, field: impl Into<String>) -> Self {
193        Self::Config {
194            message: message.into(),
195            field: Some(field.into()),
196        }
197    }
198
199    /// Create a new parse error
200    pub fn parse(language: impl Into<String>, message: impl Into<String>) -> Self {
201        Self::Parse {
202            language: language.into(),
203            message: message.into(),
204            file_path: None,
205            line: None,
206            column: None,
207        }
208    }
209
210    /// Create a new parse error with file context
211    pub fn parse_with_location(
212        language: impl Into<String>,
213        message: impl Into<String>,
214        file_path: impl Into<String>,
215        line: Option<usize>,
216        column: Option<usize>,
217    ) -> Self {
218        Self::Parse {
219            language: language.into(),
220            message: message.into(),
221            file_path: Some(file_path.into()),
222            line,
223            column,
224        }
225    }
226
227    /// Create a new mathematical error
228    pub fn math(message: impl Into<String>) -> Self {
229        Self::Math {
230            message: message.into(),
231            context: None,
232        }
233    }
234
235    /// Create a new mathematical error with context
236    pub fn math_with_context(message: impl Into<String>, context: impl Into<String>) -> Self {
237        Self::Math {
238            message: message.into(),
239            context: Some(context.into()),
240        }
241    }
242
243    /// Create a new graph analysis error
244    pub fn graph(message: impl Into<String>) -> Self {
245        Self::Graph {
246            message: message.into(),
247            element: None,
248        }
249    }
250
251    /// Create a new LSH error
252    pub fn lsh(message: impl Into<String>) -> Self {
253        Self::Lsh {
254            message: message.into(),
255            parameters: None,
256        }
257    }
258
259    /// Create a new pipeline error
260    pub fn pipeline(stage: impl Into<String>, message: impl Into<String>) -> Self {
261        Self::Pipeline {
262            stage: stage.into(),
263            message: message.into(),
264            processed_count: None,
265        }
266    }
267
268    /// Create a new validation error
269    pub fn validation(message: impl Into<String>) -> Self {
270        Self::Validation {
271            message: message.into(),
272            field: None,
273            expected: None,
274            actual: None,
275        }
276    }
277
278    /// Create a new feature unavailable error
279    pub fn feature_unavailable(feature: impl Into<String>, reason: impl Into<String>) -> Self {
280        Self::FeatureUnavailable {
281            feature: feature.into(),
282            reason: Some(reason.into()),
283        }
284    }
285
286    /// Create a new internal error
287    pub fn internal(message: impl Into<String>) -> Self {
288        Self::Internal {
289            message: message.into(),
290            context: None,
291        }
292    }
293
294    /// Create a new unsupported error
295    pub fn unsupported(message: impl Into<String>) -> Self {
296        Self::Unsupported {
297            message: message.into(),
298        }
299    }
300
301    /// Add context to an existing error
302    pub fn with_context(mut self, context: impl Into<String>) -> Self {
303        match &mut self {
304            Self::Math { context: ctx, .. } | Self::Internal { context: ctx, .. } => {
305                *ctx = Some(context.into());
306            }
307            _ => {} // Other variants handle context differently
308        }
309        self
310    }
311}
312
313// Implement From traits for common error types
314
315/// Conversion from [`io::Error`] to [`ValknutError`].
316impl From<io::Error> for ValknutError {
317    /// Converts an I/O error into a [`ValknutError::Io`] variant.
318    fn from(err: io::Error) -> Self {
319        Self::io("I/O operation failed", err)
320    }
321}
322
323/// Conversion from [`serde_json::Error`] to [`ValknutError`].
324impl From<serde_json::Error> for ValknutError {
325    /// Converts a JSON error into a [`ValknutError::Serialization`] variant.
326    fn from(err: serde_json::Error) -> Self {
327        Self::Serialization {
328            message: format!("JSON serialization failed: {err}"),
329            data_type: Some("JSON".to_string()),
330            source: Some(Box::new(err)),
331        }
332    }
333}
334
335/// Conversion from [`serde_yaml::Error`] to [`ValknutError`].
336impl From<serde_yaml::Error> for ValknutError {
337    /// Converts a YAML error into a [`ValknutError::Serialization`] variant.
338    fn from(err: serde_yaml::Error) -> Self {
339        Self::Serialization {
340            message: format!("YAML serialization failed: {err}"),
341            data_type: Some("YAML".to_string()),
342            source: Some(Box::new(err)),
343        }
344    }
345}
346
347/// Conversion from [`ParseIntError`] to [`ValknutError`].
348impl From<ParseIntError> for ValknutError {
349    /// Converts an integer parse error into a [`ValknutError::Validation`] variant.
350    fn from(err: ParseIntError) -> Self {
351        Self::validation(format!("Invalid integer: {err}"))
352    }
353}
354
355/// Conversion from [`ParseFloatError`] to [`ValknutError`].
356impl From<ParseFloatError> for ValknutError {
357    /// Converts a float parse error into a [`ValknutError::Validation`] variant.
358    fn from(err: ParseFloatError) -> Self {
359        Self::validation(format!("Invalid float: {err}"))
360    }
361}
362
363/// Conversion from [`Utf8Error`] to [`ValknutError`].
364impl From<Utf8Error> for ValknutError {
365    /// Converts a UTF-8 error into a [`ValknutError::Parse`] variant.
366    fn from(err: Utf8Error) -> Self {
367        Self::parse("unknown", format!("UTF-8 encoding error: {err}"))
368    }
369}
370
371/// Helper macro for creating context-aware errors
372#[macro_export]
373macro_rules! valknut_error {
374    ($kind:ident, $msg:expr) => {
375        $crate::core::errors::ValknutError::$kind($msg.to_string())
376    };
377    ($kind:ident, $msg:expr, $($arg:tt)*) => {
378        $crate::core::errors::ValknutError::$kind(format!($msg, $($arg)*))
379    };
380}
381
382/// Result extension trait for adding context to errors
383pub trait ResultExt<T> {
384    /// Add context to an error result
385    fn with_context<F>(self, f: F) -> Result<T>
386    where
387        F: FnOnce() -> String;
388
389    /// Add static context to an error result
390    fn context(self, msg: &'static str) -> Result<T>;
391}
392
393/// [`ResultExt`] implementation for results with errors convertible to [`ValknutError`].
394impl<T, E> ResultExt<T> for std::result::Result<T, E>
395where
396    E: Into<ValknutError>,
397{
398    /// Adds dynamic context to an error result using a closure.
399    fn with_context<F>(self, f: F) -> Result<T>
400    where
401        F: FnOnce() -> String,
402    {
403        self.map_err(|e| e.into().with_context(f()))
404    }
405
406    /// Adds static context to an error result.
407    fn context(self, msg: &'static str) -> Result<T> {
408        self.map_err(|e| e.into().with_context(msg))
409    }
410}
411
412/// Canonical error mapping adapters for [`ValknutError`].
413///
414/// These methods create closures suitable for use with `Result::map_err`.
415impl ValknutError {
416    /// Create error mapping adapter for I/O operations with custom message
417    pub fn map_io(message: impl Into<String>) -> impl FnOnce(std::io::Error) -> Self {
418        move |e| Self::io(message, e)
419    }
420
421    /// Create error mapping adapter for serialization operations
422    pub fn map_serialization(
423        operation: impl Into<String>,
424    ) -> impl FnOnce(Box<dyn std::error::Error + Send + Sync>) -> Self {
425        move |e| Self::Serialization {
426            message: format!("Serialization failed during {}: {}", operation.into(), e),
427            data_type: None,
428            source: Some(e),
429        }
430    }
431
432    /// Create error mapping adapter for JSON parsing operations
433    pub fn map_json_parse(context: impl Into<String>) -> impl FnOnce(serde_json::Error) -> Self {
434        move |e| Self::internal(format!("Failed to parse JSON {}: {}", context.into(), e))
435    }
436
437    /// Create error mapping adapter for internal operations with context
438    pub fn map_internal(
439        operation: impl Into<String>,
440    ) -> impl FnOnce(Box<dyn std::error::Error + Send + Sync>) -> Self {
441        move |e| Self::internal(format!("Internal error during {}: {}", operation.into(), e))
442    }
443
444    /// Create error mapping adapter for generic operations with error display
445    pub fn map_generic<E>(operation: impl Into<String>) -> impl FnOnce(E) -> Self
446    where
447        E: std::fmt::Display,
448    {
449        move |e| Self::internal(format!("Failed during {}: {}", operation.into(), e))
450    }
451}
452
453/// Extension trait for common error mapping patterns
454pub trait ValknutResultExt<T> {
455    /// Map I/O errors with a custom message
456    fn map_io_err(self, message: impl Into<String>) -> Result<T>;
457
458    /// Map JSON parsing errors with context
459    fn map_json_err(self, context: impl Into<String>) -> Result<T>;
460
461    /// Map generic errors with operation context
462    fn map_generic_err(self, operation: impl Into<String>) -> Result<T>;
463}
464
465/// [`ValknutResultExt`] implementation for results with displayable errors.
466impl<T, E> ValknutResultExt<T> for std::result::Result<T, E>
467where
468    E: std::fmt::Display,
469{
470    /// Maps errors to an internal error with an I/O-style message prefix.
471    fn map_io_err(self, message: impl Into<String>) -> Result<T> {
472        self.map_err(|e| ValknutError::internal(format!("{}: {}", message.into(), e)))
473    }
474
475    /// Maps errors to an internal error with JSON context.
476    fn map_json_err(self, context: impl Into<String>) -> Result<T> {
477        self.map_err(|e| ValknutError::internal(format!("JSON error in {}: {}", context.into(), e)))
478    }
479
480    /// Maps errors to an internal error with operation context.
481    fn map_generic_err(self, operation: impl Into<String>) -> Result<T> {
482        self.map_err(|e| {
483            ValknutError::internal(format!("Failed during {}: {}", operation.into(), e))
484        })
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491    use std::num::{ParseFloatError, ParseIntError};
492
493    #[test]
494    fn test_error_creation() {
495        let err = ValknutError::config("Invalid configuration");
496        assert!(matches!(err, ValknutError::Config { .. }));
497
498        let err = ValknutError::parse("python", "Syntax error");
499        assert!(matches!(err, ValknutError::Parse { .. }));
500    }
501
502    #[test]
503    fn test_error_with_context() {
504        let err =
505            ValknutError::internal("Something went wrong").with_context("During file processing");
506
507        if let ValknutError::Internal { context, .. } = err {
508            assert_eq!(context, Some("During file processing".to_string()));
509        } else {
510            panic!("Expected Internal error");
511        }
512    }
513
514    #[test]
515    fn test_result_extension() {
516        let result: std::result::Result<i32, std::io::Error> = Err(std::io::Error::new(
517            std::io::ErrorKind::NotFound,
518            "File not found",
519        ));
520
521        let valknut_result = result.context("Failed to read configuration file");
522        assert!(valknut_result.is_err());
523    }
524
525    #[test]
526    fn test_io_error_creation() {
527        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Access denied");
528        let err = ValknutError::io("Failed to write file", io_err);
529
530        if let ValknutError::Io { message, source } = &err {
531            assert_eq!(message, "Failed to write file");
532            assert_eq!(source.kind(), std::io::ErrorKind::PermissionDenied);
533        } else {
534            panic!("Expected Io error");
535        }
536    }
537
538    #[test]
539    fn test_config_field_error() {
540        let err = ValknutError::config_field("Invalid value", "max_files");
541
542        if let ValknutError::Config { message, field } = err {
543            assert_eq!(message, "Invalid value");
544            assert_eq!(field, Some("max_files".to_string()));
545        } else {
546            panic!("Expected Config error");
547        }
548    }
549
550    #[test]
551    fn test_parse_with_location() {
552        let err = ValknutError::parse_with_location(
553            "rust",
554            "Missing semicolon",
555            "main.rs",
556            Some(42),
557            Some(10),
558        );
559
560        if let ValknutError::Parse {
561            language,
562            message,
563            file_path,
564            line,
565            column,
566        } = err
567        {
568            assert_eq!(language, "rust");
569            assert_eq!(message, "Missing semicolon");
570            assert_eq!(file_path, Some("main.rs".to_string()));
571            assert_eq!(line, Some(42));
572            assert_eq!(column, Some(10));
573        } else {
574            panic!("Expected Parse error");
575        }
576    }
577
578    #[test]
579    fn test_math_with_context() {
580        let err = ValknutError::math_with_context("Division by zero", "normalize_features");
581
582        if let ValknutError::Math { message, context } = err {
583            assert_eq!(message, "Division by zero");
584            assert_eq!(context, Some("normalize_features".to_string()));
585        } else {
586            panic!("Expected Math error");
587        }
588    }
589
590    #[test]
591    fn test_graph_error() {
592        let err = ValknutError::graph("Cycle detected");
593
594        if let ValknutError::Graph { message, element } = err {
595            assert_eq!(message, "Cycle detected");
596            assert_eq!(element, None);
597        } else {
598            panic!("Expected Graph error");
599        }
600    }
601
602    #[test]
603    fn test_lsh_error() {
604        let err = ValknutError::lsh("Invalid hash function");
605
606        if let ValknutError::Lsh {
607            message,
608            parameters,
609        } = err
610        {
611            assert_eq!(message, "Invalid hash function");
612            assert_eq!(parameters, None);
613        } else {
614            panic!("Expected Lsh error");
615        }
616    }
617
618    #[test]
619    fn test_pipeline_error() {
620        let err = ValknutError::pipeline("feature_extraction", "Timeout exceeded");
621
622        if let ValknutError::Pipeline {
623            stage,
624            message,
625            processed_count,
626        } = err
627        {
628            assert_eq!(stage, "feature_extraction");
629            assert_eq!(message, "Timeout exceeded");
630            assert_eq!(processed_count, None);
631        } else {
632            panic!("Expected Pipeline error");
633        }
634    }
635
636    #[test]
637    fn test_validation_error() {
638        let err = ValknutError::validation("Invalid range");
639
640        if let ValknutError::Validation {
641            message,
642            field,
643            expected,
644            actual,
645        } = err
646        {
647            assert_eq!(message, "Invalid range");
648            assert_eq!(field, None);
649            assert_eq!(expected, None);
650            assert_eq!(actual, None);
651        } else {
652            panic!("Expected Validation error");
653        }
654    }
655
656    #[test]
657    fn test_feature_unavailable() {
658        let err = ValknutError::feature_unavailable("SIMD operations", "CPU does not support AVX2");
659
660        if let ValknutError::FeatureUnavailable { feature, reason } = err {
661            assert_eq!(feature, "SIMD operations");
662            assert_eq!(reason, Some("CPU does not support AVX2".to_string()));
663        } else {
664            panic!("Expected FeatureUnavailable error");
665        }
666    }
667
668    #[test]
669    fn test_unsupported_error() {
670        let err = ValknutError::unsupported("Language not supported");
671
672        if let ValknutError::Unsupported { message } = err {
673            assert_eq!(message, "Language not supported");
674        } else {
675            panic!("Expected Unsupported error");
676        }
677    }
678
679    #[test]
680    fn test_from_io_error() {
681        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
682        let valknut_err: ValknutError = io_err.into();
683
684        assert!(matches!(valknut_err, ValknutError::Io { .. }));
685    }
686
687    #[test]
688    fn test_from_json_error() {
689        let json_err = serde_json::from_str::<i32>("invalid json").unwrap_err();
690        let valknut_err: ValknutError = json_err.into();
691
692        if let ValknutError::Serialization { data_type, .. } = valknut_err {
693            assert_eq!(data_type, Some("JSON".to_string()));
694        } else {
695            panic!("Expected Serialization error");
696        }
697    }
698
699    #[test]
700    fn test_from_yaml_error() {
701        let yaml_err = serde_yaml::from_str::<i32>("invalid: yaml: content").unwrap_err();
702        let valknut_err: ValknutError = yaml_err.into();
703
704        if let ValknutError::Serialization { data_type, .. } = valknut_err {
705            assert_eq!(data_type, Some("YAML".to_string()));
706        } else {
707            panic!("Expected Serialization error");
708        }
709    }
710
711    #[test]
712    fn test_from_parse_int_error() {
713        let parse_err = "not_a_number".parse::<i32>().unwrap_err();
714        let valknut_err: ValknutError = parse_err.into();
715
716        assert!(matches!(valknut_err, ValknutError::Validation { .. }));
717    }
718
719    #[test]
720    fn test_from_parse_float_error() {
721        let parse_err = "not_a_float".parse::<f64>().unwrap_err();
722        let valknut_err: ValknutError = parse_err.into();
723
724        assert!(matches!(valknut_err, ValknutError::Validation { .. }));
725    }
726
727    #[test]
728    fn test_from_utf8_error() {
729        let invalid_utf8 = vec![0, 159, 146, 150]; // Invalid UTF-8 sequence
730        let utf8_err = std::str::from_utf8(&invalid_utf8).unwrap_err();
731        let valknut_err: ValknutError = utf8_err.into();
732
733        assert!(matches!(valknut_err, ValknutError::Parse { .. }));
734    }
735
736    #[test]
737    fn test_with_context_math_error() {
738        let mut err = ValknutError::math("Overflow occurred");
739        err = err.with_context("In statistical calculation");
740
741        if let ValknutError::Math { context, .. } = err {
742            assert_eq!(context, Some("In statistical calculation".to_string()));
743        } else {
744            panic!("Expected Math error with context");
745        }
746    }
747
748    #[test]
749    fn test_with_context_non_contextual_error() {
750        let err = ValknutError::config("Bad config");
751        let err_with_context = err.with_context("Should not change");
752
753        // Config errors don't support context, so it should remain unchanged
754        if let ValknutError::Config { message, .. } = err_with_context {
755            assert_eq!(message, "Bad config");
756        } else {
757            panic!("Expected Config error");
758        }
759    }
760
761    #[test]
762    fn test_result_ext_with_context() {
763        let result: std::result::Result<i32, std::io::Error> = Err(std::io::Error::new(
764            std::io::ErrorKind::InvalidInput,
765            "Bad input",
766        ));
767
768        let valknut_result = result.with_context(|| "Processing failed".to_string());
769        assert!(valknut_result.is_err());
770
771        // Verify the error was converted and context was added
772        let err = valknut_result.unwrap_err();
773        assert!(matches!(err, ValknutError::Io { .. }));
774    }
775
776    #[test]
777    fn test_error_display_formatting() {
778        let err = ValknutError::parse_with_location(
779            "python",
780            "Syntax error",
781            "test.py",
782            Some(10),
783            Some(5),
784        );
785        let display = format!("{}", err);
786        assert!(display.contains("Parse error in python"));
787        assert!(display.contains("Syntax error"));
788    }
789
790    #[test]
791    fn test_error_debug_formatting() {
792        let err = ValknutError::config_field("Invalid threshold", "complexity_max");
793        let debug = format!("{:?}", err);
794        assert!(debug.contains("Config"));
795        assert!(debug.contains("Invalid threshold"));
796        assert!(debug.contains("complexity_max"));
797    }
798}