scirs2_graph/
error.rs

1//! Error types for the graph processing module
2//!
3//! This module provides comprehensive error handling for graph operations,
4//! including detailed context information and recovery suggestions.
5
6use std::fmt;
7use thiserror::Error;
8
9/// Error type for graph processing operations
10///
11/// Provides detailed error information with context and suggestions for recovery.
12/// All errors include location information when possible.
13#[derive(Error, Debug)]
14pub enum GraphError {
15    /// Node not found in the graph
16    #[error("Node {node} not found in graph with {graph_size} nodes. Context: {context}")]
17    NodeNotFound {
18        /// The node that was not found
19        node: String,
20        /// Size of the graph for context
21        graph_size: usize,
22        /// Additional context about the operation
23        context: String,
24    },
25
26    /// Edge not found in the graph
27    #[error("Edge ({src_node}, {target}) not found in graph. Context: {context}")]
28    EdgeNotFound {
29        /// Source node of the edge
30        src_node: String,
31        /// Target node of the edge
32        target: String,
33        /// Additional context about the operation
34        context: String,
35    },
36
37    /// Invalid parameter provided to an operation
38    #[error("Invalid parameter '{param}' with value '{value}'. Expected: {expected}. Context: {context}")]
39    InvalidParameter {
40        /// Parameter name
41        param: String,
42        /// Provided value
43        value: String,
44        /// Expected value or range
45        expected: String,
46        /// Additional context
47        context: String,
48    },
49
50    /// Algorithm failed to converge or complete
51    #[error("Algorithm '{algorithm}' failed: {reason}. Iterations: {iterations}, Tolerance: {tolerance}")]
52    AlgorithmFailure {
53        /// Name of the algorithm
54        algorithm: String,
55        /// Reason for failure
56        reason: String,
57        /// Number of iterations completed
58        iterations: usize,
59        /// Tolerance used
60        tolerance: f64,
61    },
62
63    /// I/O operation failed
64    #[error("I/O error for path '{path}': {source}")]
65    IOError {
66        /// File path that caused the error
67        path: String,
68        /// Underlying I/O error
69        #[source]
70        source: std::io::Error,
71    },
72
73    /// Memory allocation or usage error
74    #[error("Memory error: requested {requested} bytes, available {available} bytes. Context: {context}")]
75    MemoryError {
76        /// Requested memory in bytes
77        requested: usize,
78        /// Available memory in bytes
79        available: usize,
80        /// Additional context
81        context: String,
82    },
83
84    /// Algorithm did not converge within specified limits
85    #[error("Convergence error in '{algorithm}': completed {iterations} iterations with tolerance {tolerance}, threshold {threshold}")]
86    ConvergenceError {
87        /// Algorithm name
88        algorithm: String,
89        /// Iterations completed
90        iterations: usize,
91        /// Final tolerance achieved
92        tolerance: f64,
93        /// Required threshold
94        threshold: f64,
95    },
96
97    /// Graph structure is invalid for the operation
98    #[error("Graph structure error: expected {expected}, found {found}. Context: {context}")]
99    GraphStructureError {
100        /// Expected graph property
101        expected: String,
102        /// Actual graph property
103        found: String,
104        /// Additional context
105        context: String,
106    },
107
108    /// No path exists between nodes
109    #[error(
110        "No path found from {src_node} to {target} in graph with {nodes} nodes and {edges} edges"
111    )]
112    NoPath {
113        /// Source node
114        src_node: String,
115        /// Target node
116        target: String,
117        /// Number of nodes in graph
118        nodes: usize,
119        /// Number of edges in graph
120        edges: usize,
121    },
122
123    /// Cycle detected when acyclic graph expected
124    #[error(
125        "Cycle detected in graph starting from node {start_node}. Cycle length: {cycle_length}"
126    )]
127    CycleDetected {
128        /// Node where cycle starts
129        start_node: String,
130        /// Length of the detected cycle
131        cycle_length: usize,
132    },
133
134    /// Linear algebra operation failed
135    #[error("Linear algebra error in operation '{operation}': {details}")]
136    LinAlgError {
137        /// Operation that failed
138        operation: String,
139        /// Error details
140        details: String,
141    },
142
143    /// Sparse matrix operation failed
144    #[error("Sparse matrix error: {details}")]
145    SparseError {
146        /// Error details
147        details: String,
148    },
149
150    /// Core module error
151    #[error("Core module error: {0}")]
152    CoreError(#[from] scirs2_core::error::CoreError),
153
154    /// Serialization/deserialization failed
155    #[error("Serialization error for format '{format}': {details}")]
156    SerializationError {
157        /// Data format (JSON, bincode, etc.)
158        format: String,
159        /// Error details
160        details: String,
161    },
162
163    /// Invalid graph attribute
164    #[error("Invalid attribute '{attribute}' for {target_type}: {details}")]
165    InvalidAttribute {
166        /// Attribute name
167        attribute: String,
168        /// Target type (node, edge, graph)
169        target_type: String,
170        /// Error details
171        details: String,
172    },
173
174    /// Computation was cancelled or interrupted
175    #[error("Operation '{operation}' was cancelled after {elapsed_time} seconds")]
176    Cancelled {
177        /// Operation name
178        operation: String,
179        /// Time elapsed before cancellation
180        elapsed_time: f64,
181    },
182
183    /// Thread safety or concurrency error
184    #[error("Concurrency error in '{operation}': {details}")]
185    ConcurrencyError {
186        /// Operation name
187        operation: String,
188        /// Error details
189        details: String,
190    },
191
192    /// Invalid graph format or version
193    #[error("Format error: unsupported format '{format}' version {version}. Supported versions: {supported}")]
194    FormatError {
195        /// Format name
196        format: String,
197        /// Version found
198        version: String,
199        /// Supported versions
200        supported: String,
201    },
202
203    /// Invalid graph structure (legacy error for backward compatibility)
204    #[error("Invalid graph: {0}")]
205    InvalidGraph(String),
206
207    /// Algorithm error (legacy error for backward compatibility)
208    #[error("Algorithm error: {0}")]
209    AlgorithmError(String),
210
211    /// Computation error (legacy error for backward compatibility)
212    #[error("Computation error: {0}")]
213    ComputationError(String),
214
215    /// Generic error for backward compatibility
216    #[error("{0}")]
217    Other(String),
218}
219
220impl GraphError {
221    /// Create a NodeNotFound error with minimal context
222    pub fn node_not_found<T: fmt::Display>(node: T) -> Self {
223        Self::NodeNotFound {
224            node: node.to_string(),
225            graph_size: 0,
226            context: "Node lookup operation".to_string(),
227        }
228    }
229
230    /// Create a NodeNotFound error with full context
231    pub fn node_not_found_with_context<T: fmt::Display>(
232        node: T,
233        graph_size: usize,
234        context: &str,
235    ) -> Self {
236        Self::NodeNotFound {
237            node: node.to_string(),
238            graph_size,
239            context: context.to_string(),
240        }
241    }
242
243    /// Create an EdgeNotFound error with minimal context
244    pub fn edge_not_found<S: fmt::Display, T: fmt::Display>(source: S, target: T) -> Self {
245        Self::EdgeNotFound {
246            src_node: source.to_string(),
247            target: target.to_string(),
248            context: "Edge lookup operation".to_string(),
249        }
250    }
251
252    /// Create an EdgeNotFound error with full context
253    pub fn edge_not_found_with_context<S: fmt::Display, T: fmt::Display>(
254        source: S,
255        target: T,
256        context: &str,
257    ) -> Self {
258        Self::EdgeNotFound {
259            src_node: source.to_string(),
260            target: target.to_string(),
261            context: context.to_string(),
262        }
263    }
264
265    /// Create an InvalidParameter error
266    pub fn invalid_parameter<P: fmt::Display, V: fmt::Display, E: fmt::Display>(
267        param: P,
268        value: V,
269        expected: E,
270    ) -> Self {
271        Self::InvalidParameter {
272            param: param.to_string(),
273            value: value.to_string(),
274            expected: expected.to_string(),
275            context: "Parameter validation".to_string(),
276        }
277    }
278
279    /// Create an AlgorithmFailure error
280    pub fn algorithm_failure<A: fmt::Display, R: fmt::Display>(
281        algorithm: A,
282        reason: R,
283        iterations: usize,
284        tolerance: f64,
285    ) -> Self {
286        Self::AlgorithmFailure {
287            algorithm: algorithm.to_string(),
288            reason: reason.to_string(),
289            iterations,
290            tolerance,
291        }
292    }
293
294    /// Create a MemoryError
295    pub fn memory_error(requested: usize, available: usize, context: &str) -> Self {
296        Self::MemoryError {
297            requested,
298            available,
299            context: context.to_string(),
300        }
301    }
302
303    /// Create a ConvergenceError
304    pub fn convergence_error<A: fmt::Display>(
305        algorithm: A,
306        iterations: usize,
307        tolerance: f64,
308        threshold: f64,
309    ) -> Self {
310        Self::ConvergenceError {
311            algorithm: algorithm.to_string(),
312            iterations,
313            tolerance,
314            threshold,
315        }
316    }
317
318    /// Create a GraphStructureError
319    pub fn graph_structure_error<E: fmt::Display, F: fmt::Display>(
320        expected: E,
321        found: F,
322        context: &str,
323    ) -> Self {
324        Self::GraphStructureError {
325            expected: expected.to_string(),
326            found: found.to_string(),
327            context: context.to_string(),
328        }
329    }
330
331    /// Create a NoPath error
332    pub fn no_path<S: fmt::Display, T: fmt::Display>(
333        source: S,
334        target: T,
335        nodes: usize,
336        edges: usize,
337    ) -> Self {
338        Self::NoPath {
339            src_node: source.to_string(),
340            target: target.to_string(),
341            nodes,
342            edges,
343        }
344    }
345
346    /// Check if this error is recoverable
347    pub fn is_recoverable(&self) -> bool {
348        match self {
349            GraphError::NodeNotFound { .. } => true,
350            GraphError::EdgeNotFound { .. } => true,
351            GraphError::NoPath { .. } => true,
352            GraphError::InvalidParameter { .. } => true,
353            GraphError::ConvergenceError { .. } => true,
354            GraphError::Cancelled { .. } => true,
355            GraphError::AlgorithmFailure { .. } => false,
356            GraphError::GraphStructureError { .. } => false,
357            GraphError::CycleDetected { .. } => false,
358            GraphError::LinAlgError { .. } => false,
359            GraphError::SparseError { .. } => false,
360            GraphError::SerializationError { .. } => false,
361            GraphError::InvalidAttribute { .. } => true,
362            GraphError::ConcurrencyError { .. } => false,
363            GraphError::FormatError { .. } => false,
364            GraphError::InvalidGraph(_) => false,
365            GraphError::AlgorithmError(_) => false,
366            GraphError::MemoryError { .. } => false,
367            GraphError::IOError { .. } => false,
368            GraphError::CoreError(_) => false,
369            GraphError::ComputationError(_) => false,
370            GraphError::Other(_) => false,
371        }
372    }
373
374    /// Get suggestions for error recovery
375    pub fn recovery_suggestions(&self) -> Vec<String> {
376        match self {
377            GraphError::NodeNotFound { .. } => vec![
378                "Check that the node exists in the graph".to_string(),
379                "Verify node ID format and type".to_string(),
380                "Use graph.has_node() to check existence first".to_string(),
381            ],
382            GraphError::EdgeNotFound { .. } => vec![
383                "Check that both nodes exist in the graph".to_string(),
384                "Verify edge direction for directed graphs".to_string(),
385                "Use graph.has_edge() to check existence first".to_string(),
386            ],
387            GraphError::NoPath { .. } => vec![
388                "Check if graph is connected".to_string(),
389                "Verify that both nodes exist".to_string(),
390                "Consider using weakly connected components for directed graphs".to_string(),
391            ],
392            GraphError::AlgorithmFailure { algorithm, .. } => match algorithm.as_str() {
393                "pagerank" => vec![
394                    "Increase iteration limit".to_string(),
395                    "Reduce tolerance threshold".to_string(),
396                    "Check for disconnected components".to_string(),
397                ],
398                "community_detection" => vec![
399                    "Try different resolution parameters".to_string(),
400                    "Ensure graph has edges".to_string(),
401                    "Consider preprocessing to remove isolates".to_string(),
402                ],
403                _ => vec!["Adjust algorithm parameters".to_string()],
404            },
405            GraphError::MemoryError { .. } => vec![
406                "Use streaming algorithms for large graphs".to_string(),
407                "Enable memory optimization features".to_string(),
408                "Process graph in smaller chunks".to_string(),
409            ],
410            GraphError::ConvergenceError { .. } => vec![
411                "Increase maximum iterations".to_string(),
412                "Adjust tolerance threshold".to_string(),
413                "Check for numerical stability issues".to_string(),
414            ],
415            _ => vec!["Check input parameters and graph structure".to_string()],
416        }
417    }
418
419    /// Get the error category for metrics and logging
420    pub fn category(&self) -> &'static str {
421        match self {
422            GraphError::NodeNotFound { .. } | GraphError::EdgeNotFound { .. } => "lookup",
423            GraphError::InvalidParameter { .. } => "validation",
424            GraphError::AlgorithmFailure { .. } | GraphError::ConvergenceError { .. } => {
425                "algorithm"
426            }
427            GraphError::IOError { .. } => "io",
428            GraphError::MemoryError { .. } => "memory",
429            GraphError::GraphStructureError { .. } => "structure",
430            GraphError::NoPath { .. } => "connectivity",
431            GraphError::CycleDetected { .. } => "topology",
432            GraphError::SerializationError { .. } => "serialization",
433            GraphError::Cancelled { .. } => "cancellation",
434            GraphError::ConcurrencyError { .. } => "concurrency",
435            GraphError::FormatError { .. } => "format",
436            _ => "other",
437        }
438    }
439}
440
441/// Result type for graph processing operations
442pub type Result<T> = std::result::Result<T, GraphError>;
443
444/// Convert std::io::Error to GraphError with path context
445impl From<std::io::Error> for GraphError {
446    fn from(err: std::io::Error) -> Self {
447        GraphError::IOError {
448            path: "unknown".to_string(),
449            source: err,
450        }
451    }
452}
453
454/// Error context helper for adding operation context to errors
455pub struct ErrorContext {
456    operation: String,
457    graph_info: Option<(usize, usize)>, // (nodes, edges)
458}
459
460impl ErrorContext {
461    /// Create new error context
462    pub fn new(operation: &str) -> Self {
463        Self {
464            operation: operation.to_string(),
465            graph_info: None,
466        }
467    }
468
469    /// Add graph size information
470    pub fn with_graph_info(mut self, nodes: usize, edges: usize) -> Self {
471        self.graph_info = Some((nodes, edges));
472        self
473    }
474
475    /// Wrap a result with context information
476    pub fn wrap<T>(self, result: Result<T>) -> Result<T> {
477        result.map_err(|err| self.add_context(err))
478    }
479
480    /// Add context to an existing error
481    fn add_context(self, mut err: GraphError) -> GraphError {
482        match &mut err {
483            GraphError::NodeNotFound { context, .. } => {
484                if context == "Node lookup operation" {
485                    *context = self.operation;
486                }
487            }
488            GraphError::EdgeNotFound { context, .. } => {
489                if context == "Edge lookup operation" {
490                    *context = self.operation;
491                }
492            }
493            GraphError::InvalidParameter { context, .. } => {
494                if context == "Parameter validation" {
495                    *context = self.operation;
496                }
497            }
498            GraphError::GraphStructureError { context, .. } => {
499                *context = self.operation;
500            }
501            _ => {}
502        }
503        err
504    }
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510
511    #[test]
512    fn test_error_creation() {
513        let err = GraphError::node_not_found(42);
514        assert!(matches!(err, GraphError::NodeNotFound { .. }));
515        assert!(err.is_recoverable());
516        assert_eq!(err.category(), "lookup");
517    }
518
519    #[test]
520    fn test_error_context() {
521        let _ctx = ErrorContext::new("PageRank computation").with_graph_info(100, 250);
522        let err = GraphError::convergence_error("pagerank", 100, 1e-3, 1e-6);
523        let suggestions = err.recovery_suggestions();
524        assert!(!suggestions.is_empty());
525    }
526
527    #[test]
528    fn test_error_categories() {
529        assert_eq!(GraphError::node_not_found(1).category(), "lookup");
530        assert_eq!(
531            GraphError::algorithm_failure("test", "failed", 0, 1e-6).category(),
532            "algorithm"
533        );
534        assert_eq!(
535            GraphError::memory_error(1000, 500, "test").category(),
536            "memory"
537        );
538    }
539}