1use std::fmt;
7use thiserror::Error;
8
9#[derive(Error, Debug)]
14pub enum GraphError {
15 #[error("Node {node} not found in graph with {graph_size} nodes. Context: {context}")]
17 NodeNotFound {
18 node: String,
20 graph_size: usize,
22 context: String,
24 },
25
26 #[error("Edge ({src_node}, {target}) not found in graph. Context: {context}")]
28 EdgeNotFound {
29 src_node: String,
31 target: String,
33 context: String,
35 },
36
37 #[error("Invalid parameter '{param}' with value '{value}'. Expected: {expected}. Context: {context}")]
39 InvalidParameter {
40 param: String,
42 value: String,
44 expected: String,
46 context: String,
48 },
49
50 #[error("Algorithm '{algorithm}' failed: {reason}. Iterations: {iterations}, Tolerance: {tolerance}")]
52 AlgorithmFailure {
53 algorithm: String,
55 reason: String,
57 iterations: usize,
59 tolerance: f64,
61 },
62
63 #[error("I/O error for path '{path}': {source}")]
65 IOError {
66 path: String,
68 #[source]
70 source: std::io::Error,
71 },
72
73 #[error("Memory error: requested {requested} bytes, available {available} bytes. Context: {context}")]
75 MemoryError {
76 requested: usize,
78 available: usize,
80 context: String,
82 },
83
84 #[error("Convergence error in '{algorithm}': completed {iterations} iterations with tolerance {tolerance}, threshold {threshold}")]
86 ConvergenceError {
87 algorithm: String,
89 iterations: usize,
91 tolerance: f64,
93 threshold: f64,
95 },
96
97 #[error("Graph structure error: expected {expected}, found {found}. Context: {context}")]
99 GraphStructureError {
100 expected: String,
102 found: String,
104 context: String,
106 },
107
108 #[error(
110 "No path found from {src_node} to {target} in graph with {nodes} nodes and {edges} edges"
111 )]
112 NoPath {
113 src_node: String,
115 target: String,
117 nodes: usize,
119 edges: usize,
121 },
122
123 #[error(
125 "Cycle detected in graph starting from node {start_node}. Cycle length: {cycle_length}"
126 )]
127 CycleDetected {
128 start_node: String,
130 cycle_length: usize,
132 },
133
134 #[error("Linear algebra error in operation '{operation}': {details}")]
136 LinAlgError {
137 operation: String,
139 details: String,
141 },
142
143 #[error("Sparse matrix error: {details}")]
145 SparseError {
146 details: String,
148 },
149
150 #[error("Core module error: {0}")]
152 CoreError(#[from] scirs2_core::error::CoreError),
153
154 #[error("Serialization error for format '{format}': {details}")]
156 SerializationError {
157 format: String,
159 details: String,
161 },
162
163 #[error("Invalid attribute '{attribute}' for {target_type}: {details}")]
165 InvalidAttribute {
166 attribute: String,
168 target_type: String,
170 details: String,
172 },
173
174 #[error("Operation '{operation}' was cancelled after {elapsed_time} seconds")]
176 Cancelled {
177 operation: String,
179 elapsed_time: f64,
181 },
182
183 #[error("Concurrency error in '{operation}': {details}")]
185 ConcurrencyError {
186 operation: String,
188 details: String,
190 },
191
192 #[error("Format error: unsupported format '{format}' version {version}. Supported versions: {supported}")]
194 FormatError {
195 format: String,
197 version: String,
199 supported: String,
201 },
202
203 #[error("Invalid graph: {0}")]
205 InvalidGraph(String),
206
207 #[error("Algorithm error: {0}")]
209 AlgorithmError(String),
210
211 #[error("Computation error: {0}")]
213 ComputationError(String),
214
215 #[error("Unsupported: {0}")]
222 Unsupported(String),
223
224 #[error("{0}")]
226 Other(String),
227}
228
229impl GraphError {
230 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 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 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 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 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 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 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 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 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 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 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 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 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
458pub type Result<T> = std::result::Result<T, GraphError>;
460
461impl 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
471pub struct ErrorContext {
473 operation: String,
474 graph_info: Option<(usize, usize)>, }
476
477impl ErrorContext {
478 pub fn new(operation: &str) -> Self {
480 Self {
481 operation: operation.to_string(),
482 graph_info: None,
483 }
484 }
485
486 pub fn with_graph_info(mut self, nodes: usize, edges: usize) -> Self {
488 self.graph_info = Some((nodes, edges));
489 self
490 }
491
492 pub fn wrap<T>(self, result: Result<T>) -> Result<T> {
494 result.map_err(|err| self.add_context(err))
495 }
496
497 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}