rust_logic_graph/error/
mod.rs

1//! Rich error handling for Rust Logic Graph
2//!
3//! This module provides comprehensive error types with:
4//! - Unique error codes for documentation lookup
5//! - Error classification (Retryable, Permanent, Transient)
6//! - Actionable suggestions for fixing errors
7//! - Rich context propagation across distributed systems
8//! - Links to troubleshooting documentation
9
10use std::fmt;
11
12/// Error classification for retry strategies
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ErrorCategory {
15    /// Error can be retried (temporary network issues, rate limits)
16    Retryable,
17    /// Error is permanent (invalid configuration, syntax errors)
18    Permanent,
19    /// Error is transient (database deadlock, temporary unavailability)
20    Transient,
21    /// Error in configuration (missing required fields, invalid values)
22    Configuration,
23}
24
25/// Context about where the error occurred in the graph execution
26#[derive(Debug, Clone)]
27pub struct ErrorContext {
28    /// Node ID where error occurred
29    pub node_id: Option<String>,
30    /// Graph name being executed
31    pub graph_name: Option<String>,
32    /// Execution step/phase
33    pub execution_step: Option<String>,
34    /// Service name (for distributed systems)
35    pub service_name: Option<String>,
36    /// Additional context key-value pairs
37    pub metadata: Vec<(String, String)>,
38}
39
40impl ErrorContext {
41    pub fn new() -> Self {
42        Self {
43            node_id: None,
44            graph_name: None,
45            execution_step: None,
46            service_name: None,
47            metadata: Vec::new(),
48        }
49    }
50
51    pub fn with_node(mut self, node_id: impl Into<String>) -> Self {
52        self.node_id = Some(node_id.into());
53        self
54    }
55
56    pub fn with_graph(mut self, graph_name: impl Into<String>) -> Self {
57        self.graph_name = Some(graph_name.into());
58        self
59    }
60
61    pub fn with_step(mut self, step: impl Into<String>) -> Self {
62        self.execution_step = Some(step.into());
63        self
64    }
65
66    pub fn with_service(mut self, service_name: impl Into<String>) -> Self {
67        self.service_name = Some(service_name.into());
68        self
69    }
70
71    pub fn add_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
72        self.metadata.push((key.into(), value.into()));
73        self
74    }
75}
76
77impl Default for ErrorContext {
78    fn default() -> Self {
79        Self::new()
80    }
81}
82
83/// Main error type for Rust Logic Graph
84#[derive(Debug)]
85pub struct RustLogicGraphError {
86    /// Unique error code (e.g., "E001", "E002")
87    pub code: String,
88    /// Human-readable error message
89    pub message: String,
90    /// Error classification for retry logic
91    pub category: ErrorCategory,
92    /// Actionable suggestion for fixing the error
93    pub suggestion: Option<String>,
94    /// Link to documentation/troubleshooting
95    pub doc_link: Option<String>,
96    /// Rich context about where error occurred
97    pub context: ErrorContext,
98    /// Underlying cause (if any)
99    pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
100}
101
102impl RustLogicGraphError {
103    /// Create a new error with code and message
104    pub fn new(code: impl Into<String>, message: impl Into<String>, category: ErrorCategory) -> Self {
105        let code = code.into();
106        let doc_link = Some(format!("https://docs.rust-logic-graph.dev/errors/{}", code));
107        
108        Self {
109            code,
110            message: message.into(),
111            category,
112            suggestion: None,
113            doc_link,
114            context: ErrorContext::new(),
115            source: None,
116        }
117    }
118
119    /// Add an actionable suggestion
120    pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
121        self.suggestion = Some(suggestion.into());
122        self
123    }
124
125    /// Add error context
126    pub fn with_context(mut self, context: ErrorContext) -> Self {
127        self.context = context;
128        self
129    }
130
131    /// Add underlying source error
132    pub fn with_source(mut self, source: impl std::error::Error + Send + Sync + 'static) -> Self {
133        self.source = Some(Box::new(source));
134        self
135    }
136
137    /// Check if error is retryable
138    pub fn is_retryable(&self) -> bool {
139        matches!(self.category, ErrorCategory::Retryable | ErrorCategory::Transient)
140    }
141
142    /// Check if error is permanent
143    pub fn is_permanent(&self) -> bool {
144        matches!(self.category, ErrorCategory::Permanent | ErrorCategory::Configuration)
145    }
146}
147
148impl fmt::Display for RustLogicGraphError {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        write!(f, "[{}] {}", self.code, self.message)?;
151
152        // Add context information
153        if let Some(ref graph) = self.context.graph_name {
154            write!(f, "\n  Graph: {}", graph)?;
155        }
156        if let Some(ref node) = self.context.node_id {
157            write!(f, "\n  Node: {}", node)?;
158        }
159        if let Some(ref step) = self.context.execution_step {
160            write!(f, "\n  Step: {}", step)?;
161        }
162        if let Some(ref service) = self.context.service_name {
163            write!(f, "\n  Service: {}", service)?;
164        }
165
166        // Add metadata
167        for (key, value) in &self.context.metadata {
168            write!(f, "\n  {}: {}", key, value)?;
169        }
170
171        // Add suggestion
172        if let Some(ref suggestion) = self.suggestion {
173            write!(f, "\n\nšŸ’” Suggestion: {}", suggestion)?;
174        }
175
176        // Add documentation link
177        if let Some(ref link) = self.doc_link {
178            write!(f, "\nšŸ“– Documentation: {}", link)?;
179        }
180
181        // Add source error
182        if let Some(ref source) = self.source {
183            write!(f, "\n\nCaused by: {}", source)?;
184        }
185
186        Ok(())
187    }
188}
189
190impl std::error::Error for RustLogicGraphError {
191    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
192        self.source.as_ref().map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
193    }
194}
195
196// Convenience constructors for common error types
197
198impl RustLogicGraphError {
199    /// Node execution error
200    pub fn node_execution_error(node_id: impl Into<String>, message: impl Into<String>) -> Self {
201        Self::new("E001", message, ErrorCategory::Retryable)
202            .with_context(ErrorContext::new().with_node(node_id))
203            .with_suggestion("Check node configuration and input data. Verify all dependencies are available.")
204    }
205
206    /// Database connection error
207    pub fn database_connection_error(message: impl Into<String>) -> Self {
208        Self::new("E002", message, ErrorCategory::Retryable)
209            .with_suggestion("Verify database connection string, credentials, and network connectivity. Check if database server is running.")
210    }
211
212    /// Rule evaluation error
213    pub fn rule_evaluation_error(message: impl Into<String>) -> Self {
214        Self::new("E003", message, ErrorCategory::Permanent)
215            .with_suggestion("Check rule syntax and ensure all required facts are present. Verify rule logic is correct.")
216    }
217
218    /// Configuration error
219    pub fn configuration_error(message: impl Into<String>) -> Self {
220        Self::new("E004", message, ErrorCategory::Configuration)
221            .with_suggestion("Review configuration file for missing or invalid values. Check against schema documentation.")
222    }
223
224    /// Timeout error
225    pub fn timeout_error(message: impl Into<String>) -> Self {
226        Self::new("E005", message, ErrorCategory::Transient)
227            .with_suggestion("Increase timeout duration or investigate performance bottlenecks. Check for slow downstream services.")
228    }
229
230    /// Graph validation error
231    pub fn graph_validation_error(message: impl Into<String>) -> Self {
232        Self::new("E006", message, ErrorCategory::Permanent)
233            .with_suggestion("Verify graph structure is valid. Check for cycles, missing nodes, or invalid edge connections.")
234    }
235
236    /// Serialization error
237    pub fn serialization_error(message: impl Into<String>) -> Self {
238        Self::new("E007", message, ErrorCategory::Permanent)
239            .with_suggestion("Check data format and ensure all required fields are present. Verify JSON/YAML syntax is valid.")
240    }
241
242    /// AI/LLM error
243    pub fn ai_error(message: impl Into<String>) -> Self {
244        Self::new("E008", message, ErrorCategory::Retryable)
245            .with_suggestion("Verify API key and model availability. Check rate limits and quota. Review prompt for issues.")
246    }
247
248    /// Cache error
249    pub fn cache_error(message: impl Into<String>) -> Self {
250        Self::new("E009", message, ErrorCategory::Transient)
251            .with_suggestion("Check cache configuration and connectivity. Verify cache backend is operational.")
252    }
253
254    /// Context error
255    pub fn context_error(message: impl Into<String>) -> Self {
256        Self::new("E010", message, ErrorCategory::Permanent)
257            .with_suggestion("Verify context data structure. Ensure required keys are present and values are correct types.")
258    }
259
260    /// Distributed system error
261    pub fn distributed_error(message: impl Into<String>, service: impl Into<String>) -> Self {
262        Self::new("E011", message, ErrorCategory::Retryable)
263            .with_context(ErrorContext::new().with_service(service))
264            .with_suggestion("Check service health and network connectivity. Verify service discovery and load balancing configuration.")
265    }
266
267    /// Transaction coordination error
268    pub fn transaction_error(message: impl Into<String>) -> Self {
269        Self::new("E012", message, ErrorCategory::Transient)
270            .with_suggestion("Review transaction logic and compensation handlers. Check for deadlocks or isolation issues.")
271    }
272}
273
274/// Result type alias for convenience
275pub type Result<T> = std::result::Result<T, RustLogicGraphError>;
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    #[test]
282    fn test_error_creation() {
283        let err = RustLogicGraphError::node_execution_error("node_1", "Failed to execute node");
284        assert_eq!(err.code, "E001");
285        assert_eq!(err.category, ErrorCategory::Retryable);
286        assert!(err.is_retryable());
287        assert!(!err.is_permanent());
288    }
289
290    #[test]
291    fn test_error_with_context() {
292        let context = ErrorContext::new()
293            .with_node("node_1")
294            .with_graph("my_graph")
295            .with_step("execution");
296
297        let err = RustLogicGraphError::new("E001", "Test error", ErrorCategory::Retryable)
298            .with_context(context);
299
300        assert_eq!(err.context.node_id, Some("node_1".to_string()));
301        assert_eq!(err.context.graph_name, Some("my_graph".to_string()));
302    }
303
304    #[test]
305    fn test_error_display() {
306        let err = RustLogicGraphError::database_connection_error("Connection timeout");
307        let display = format!("{}", err);
308        
309        assert!(display.contains("[E002]"));
310        assert!(display.contains("Connection timeout"));
311        assert!(display.contains("šŸ’” Suggestion:"));
312        assert!(display.contains("šŸ“– Documentation:"));
313    }
314
315    #[test]
316    fn test_error_categories() {
317        assert!(RustLogicGraphError::database_connection_error("test").is_retryable());
318        assert!(RustLogicGraphError::configuration_error("test").is_permanent());
319        assert!(RustLogicGraphError::timeout_error("test").is_retryable());
320        assert!(RustLogicGraphError::graph_validation_error("test").is_permanent());
321    }
322
323    #[test]
324    fn test_error_with_metadata() {
325        let context = ErrorContext::new()
326            .add_metadata("user_id", "123")
327            .add_metadata("request_id", "req_456");
328
329        let err = RustLogicGraphError::new("E001", "Test", ErrorCategory::Retryable)
330            .with_context(context);
331
332        assert_eq!(err.context.metadata.len(), 2);
333    }
334}