Skip to main content

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, OxiCode, 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    /// A requested capability is not implemented and, unlike most gaps in
216    /// this crate, genuinely is not planned to be: the feature is out of
217    /// scope for what a graph-processing library can honestly provide (e.g.
218    /// it would require hardware or a simulator this crate does not have
219    /// access to). Returned instead of silently no-op'ing or fabricating a
220    /// plausible-looking result.
221    #[error("Unsupported: {0}")]
222    Unsupported(String),
223
224    /// Generic error for backward compatibility
225    #[error("{0}")]
226    Other(String),
227}
228
229impl GraphError {
230    /// Create a NodeNotFound error with minimal context
231    pub fn node_not_found<T: fmt::Display>(node: T) -> Self {
232        Self::NodeNotFound {
233            node: node.to_string(),
234            graph_size: 0,
235            context: "Node lookup operation".to_string(),
236        }
237    }
238
239    /// Create a NodeNotFound error with full context
240    pub fn node_not_found_with_context<T: fmt::Display>(
241        node: T,
242        graph_size: usize,
243        context: &str,
244    ) -> Self {
245        Self::NodeNotFound {
246            node: node.to_string(),
247            graph_size,
248            context: context.to_string(),
249        }
250    }
251
252    /// Create an EdgeNotFound error with minimal context
253    pub fn edge_not_found<S: fmt::Display, T: fmt::Display>(source: S, target: T) -> Self {
254        Self::EdgeNotFound {
255            src_node: source.to_string(),
256            target: target.to_string(),
257            context: "Edge lookup operation".to_string(),
258        }
259    }
260
261    /// Create an EdgeNotFound error with full context
262    pub fn edge_not_found_with_context<S: fmt::Display, T: fmt::Display>(
263        source: S,
264        target: T,
265        context: &str,
266    ) -> Self {
267        Self::EdgeNotFound {
268            src_node: source.to_string(),
269            target: target.to_string(),
270            context: context.to_string(),
271        }
272    }
273
274    /// Create an InvalidParameter error
275    pub fn invalid_parameter<P: fmt::Display, V: fmt::Display, E: fmt::Display>(
276        param: P,
277        value: V,
278        expected: E,
279    ) -> Self {
280        Self::InvalidParameter {
281            param: param.to_string(),
282            value: value.to_string(),
283            expected: expected.to_string(),
284            context: "Parameter validation".to_string(),
285        }
286    }
287
288    /// Create an AlgorithmFailure error
289    pub fn algorithm_failure<A: fmt::Display, R: fmt::Display>(
290        algorithm: A,
291        reason: R,
292        iterations: usize,
293        tolerance: f64,
294    ) -> Self {
295        Self::AlgorithmFailure {
296            algorithm: algorithm.to_string(),
297            reason: reason.to_string(),
298            iterations,
299            tolerance,
300        }
301    }
302
303    /// Create a MemoryError
304    pub fn memory_error(requested: usize, available: usize, context: &str) -> Self {
305        Self::MemoryError {
306            requested,
307            available,
308            context: context.to_string(),
309        }
310    }
311
312    /// Create a ConvergenceError
313    pub fn convergence_error<A: fmt::Display>(
314        algorithm: A,
315        iterations: usize,
316        tolerance: f64,
317        threshold: f64,
318    ) -> Self {
319        Self::ConvergenceError {
320            algorithm: algorithm.to_string(),
321            iterations,
322            tolerance,
323            threshold,
324        }
325    }
326
327    /// Create a GraphStructureError
328    pub fn graph_structure_error<E: fmt::Display, F: fmt::Display>(
329        expected: E,
330        found: F,
331        context: &str,
332    ) -> Self {
333        Self::GraphStructureError {
334            expected: expected.to_string(),
335            found: found.to_string(),
336            context: context.to_string(),
337        }
338    }
339
340    /// Create a NoPath error
341    pub fn no_path<S: fmt::Display, T: fmt::Display>(
342        source: S,
343        target: T,
344        nodes: usize,
345        edges: usize,
346    ) -> Self {
347        Self::NoPath {
348            src_node: source.to_string(),
349            target: target.to_string(),
350            nodes,
351            edges,
352        }
353    }
354
355    /// Check if this error is recoverable
356    pub fn is_recoverable(&self) -> bool {
357        match self {
358            GraphError::NodeNotFound { .. } => true,
359            GraphError::EdgeNotFound { .. } => true,
360            GraphError::NoPath { .. } => true,
361            GraphError::InvalidParameter { .. } => true,
362            GraphError::ConvergenceError { .. } => true,
363            GraphError::Cancelled { .. } => true,
364            GraphError::AlgorithmFailure { .. } => false,
365            GraphError::GraphStructureError { .. } => false,
366            GraphError::CycleDetected { .. } => false,
367            GraphError::LinAlgError { .. } => false,
368            GraphError::SparseError { .. } => false,
369            GraphError::SerializationError { .. } => false,
370            GraphError::InvalidAttribute { .. } => true,
371            GraphError::ConcurrencyError { .. } => false,
372            GraphError::FormatError { .. } => false,
373            GraphError::InvalidGraph(_) => false,
374            GraphError::AlgorithmError(_) => false,
375            GraphError::MemoryError { .. } => false,
376            GraphError::IOError { .. } => false,
377            GraphError::CoreError(_) => false,
378            GraphError::ComputationError(_) => false,
379            GraphError::Unsupported(_) => false,
380            GraphError::Other(_) => false,
381        }
382    }
383
384    /// Get suggestions for error recovery
385    pub fn recovery_suggestions(&self) -> Vec<String> {
386        match self {
387            GraphError::NodeNotFound { .. } => vec![
388                "Check that the node exists in the graph".to_string(),
389                "Verify node ID format and type".to_string(),
390                "Use graph.has_node() to check existence first".to_string(),
391            ],
392            GraphError::EdgeNotFound { .. } => vec![
393                "Check that both nodes exist in the graph".to_string(),
394                "Verify edge direction for directed graphs".to_string(),
395                "Use graph.has_edge() to check existence first".to_string(),
396            ],
397            GraphError::NoPath { .. } => vec![
398                "Check if graph is connected".to_string(),
399                "Verify that both nodes exist".to_string(),
400                "Consider using weakly connected components for directed graphs".to_string(),
401            ],
402            GraphError::AlgorithmFailure { algorithm, .. } => match algorithm.as_str() {
403                "pagerank" => vec![
404                    "Increase iteration limit".to_string(),
405                    "Reduce tolerance threshold".to_string(),
406                    "Check for disconnected components".to_string(),
407                ],
408                "community_detection" => vec![
409                    "Try different resolution parameters".to_string(),
410                    "Ensure graph has edges".to_string(),
411                    "Consider preprocessing to remove isolates".to_string(),
412                ],
413                _ => vec!["Adjust algorithm parameters".to_string()],
414            },
415            GraphError::MemoryError { .. } => vec![
416                "Use streaming algorithms for large graphs".to_string(),
417                "Enable memory optimization features".to_string(),
418                "Process graph in smaller chunks".to_string(),
419            ],
420            GraphError::ConvergenceError { .. } => vec![
421                "Increase maximum iterations".to_string(),
422                "Adjust tolerance threshold".to_string(),
423                "Check for numerical stability issues".to_string(),
424            ],
425            GraphError::Unsupported(_) => vec![
426                "This capability is out of scope for this crate and is not planned; \
427                 no retry or reconfiguration will make it succeed"
428                    .to_string(),
429                "Use a CPU-based (non-accelerated) code path instead".to_string(),
430            ],
431            _ => vec!["Check input parameters and graph structure".to_string()],
432        }
433    }
434
435    /// Get the error category for metrics and logging
436    pub fn category(&self) -> &'static str {
437        match self {
438            GraphError::NodeNotFound { .. } | GraphError::EdgeNotFound { .. } => "lookup",
439            GraphError::InvalidParameter { .. } => "validation",
440            GraphError::AlgorithmFailure { .. } | GraphError::ConvergenceError { .. } => {
441                "algorithm"
442            }
443            GraphError::IOError { .. } => "io",
444            GraphError::MemoryError { .. } => "memory",
445            GraphError::GraphStructureError { .. } => "structure",
446            GraphError::NoPath { .. } => "connectivity",
447            GraphError::CycleDetected { .. } => "topology",
448            GraphError::SerializationError { .. } => "serialization",
449            GraphError::Cancelled { .. } => "cancellation",
450            GraphError::ConcurrencyError { .. } => "concurrency",
451            GraphError::FormatError { .. } => "format",
452            GraphError::Unsupported(_) => "unsupported",
453            _ => "other",
454        }
455    }
456}
457
458/// Result type for graph processing operations
459pub type Result<T> = std::result::Result<T, GraphError>;
460
461/// Convert std::io::Error to GraphError with path context
462impl From<std::io::Error> for GraphError {
463    fn from(err: std::io::Error) -> Self {
464        GraphError::IOError {
465            path: "unknown".to_string(),
466            source: err,
467        }
468    }
469}
470
471/// Error context helper for adding operation context to errors
472pub struct ErrorContext {
473    operation: String,
474    graph_info: Option<(usize, usize)>, // (nodes, edges)
475}
476
477impl ErrorContext {
478    /// Create new error context
479    pub fn new(operation: &str) -> Self {
480        Self {
481            operation: operation.to_string(),
482            graph_info: None,
483        }
484    }
485
486    /// Add graph size information
487    pub fn with_graph_info(mut self, nodes: usize, edges: usize) -> Self {
488        self.graph_info = Some((nodes, edges));
489        self
490    }
491
492    /// Wrap a result with context information
493    pub fn wrap<T>(self, result: Result<T>) -> Result<T> {
494        result.map_err(|err| self.add_context(err))
495    }
496
497    /// Add context to an existing error
498    fn add_context(self, mut err: GraphError) -> GraphError {
499        match &mut err {
500            GraphError::NodeNotFound { context, .. } if context == "Node lookup operation" => {
501                *context = self.operation;
502            }
503            GraphError::EdgeNotFound { context, .. } if context == "Edge lookup operation" => {
504                *context = self.operation;
505            }
506            GraphError::InvalidParameter { context, .. } if context == "Parameter validation" => {
507                *context = self.operation;
508            }
509            GraphError::GraphStructureError { context, .. } => {
510                *context = self.operation;
511            }
512            _ => {}
513        }
514        err
515    }
516}
517
518#[cfg(test)]
519mod tests {
520    use super::*;
521
522    #[test]
523    fn test_error_creation() {
524        let err = GraphError::node_not_found(42);
525        assert!(matches!(err, GraphError::NodeNotFound { .. }));
526        assert!(err.is_recoverable());
527        assert_eq!(err.category(), "lookup");
528    }
529
530    #[test]
531    fn test_error_context() {
532        let _ctx = ErrorContext::new("PageRank computation").with_graph_info(100, 250);
533        let err = GraphError::convergence_error("pagerank", 100, 1e-3, 1e-6);
534        let suggestions = err.recovery_suggestions();
535        assert!(!suggestions.is_empty());
536    }
537
538    #[test]
539    fn test_error_categories() {
540        assert_eq!(GraphError::node_not_found(1).category(), "lookup");
541        assert_eq!(
542            GraphError::algorithm_failure("test", "failed", 0, 1e-6).category(),
543            "algorithm"
544        );
545        assert_eq!(
546            GraphError::memory_error(1000, 500, "test").category(),
547            "memory"
548        );
549    }
550}