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("{0}")]
217 Other(String),
218}
219
220impl GraphError {
221 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 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 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 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 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 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 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 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 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 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 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 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 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
441pub type Result<T> = std::result::Result<T, GraphError>;
443
444impl 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
454pub struct ErrorContext {
456 operation: String,
457 graph_info: Option<(usize, usize)>, }
459
460impl ErrorContext {
461 pub fn new(operation: &str) -> Self {
463 Self {
464 operation: operation.to_string(),
465 graph_info: None,
466 }
467 }
468
469 pub fn with_graph_info(mut self, nodes: usize, edges: usize) -> Self {
471 self.graph_info = Some((nodes, edges));
472 self
473 }
474
475 pub fn wrap<T>(self, result: Result<T>) -> Result<T> {
477 result.map_err(|err| self.add_context(err))
478 }
479
480 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}