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(
105        code: impl Into<String>,
106        message: impl Into<String>,
107        category: ErrorCategory,
108    ) -> Self {
109        let code = code.into();
110        let doc_link = Some(format!("https://docs.rust-logic-graph.dev/errors/{}", code));
111
112        Self {
113            code,
114            message: message.into(),
115            category,
116            suggestion: None,
117            doc_link,
118            context: ErrorContext::new(),
119            source: None,
120        }
121    }
122
123    /// Add an actionable suggestion
124    pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
125        self.suggestion = Some(suggestion.into());
126        self
127    }
128
129    /// Add error context
130    pub fn with_context(mut self, context: ErrorContext) -> Self {
131        self.context = context;
132        self
133    }
134
135    /// Add underlying source error
136    pub fn with_source(mut self, source: impl std::error::Error + Send + Sync + 'static) -> Self {
137        self.source = Some(Box::new(source));
138        self
139    }
140
141    /// Check if error is retryable
142    pub fn is_retryable(&self) -> bool {
143        matches!(
144            self.category,
145            ErrorCategory::Retryable | ErrorCategory::Transient
146        )
147    }
148
149    /// Check if error is permanent
150    pub fn is_permanent(&self) -> bool {
151        matches!(
152            self.category,
153            ErrorCategory::Permanent | ErrorCategory::Configuration
154        )
155    }
156}
157
158impl fmt::Display for RustLogicGraphError {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        write!(f, "[{}] {}", self.code, self.message)?;
161
162        // Add context information
163        if let Some(ref graph) = self.context.graph_name {
164            write!(f, "\n  Graph: {}", graph)?;
165        }
166        if let Some(ref node) = self.context.node_id {
167            write!(f, "\n  Node: {}", node)?;
168        }
169        if let Some(ref step) = self.context.execution_step {
170            write!(f, "\n  Step: {}", step)?;
171        }
172        if let Some(ref service) = self.context.service_name {
173            write!(f, "\n  Service: {}", service)?;
174        }
175
176        // Add metadata
177        for (key, value) in &self.context.metadata {
178            write!(f, "\n  {}: {}", key, value)?;
179        }
180
181        // Add suggestion
182        if let Some(ref suggestion) = self.suggestion {
183            write!(f, "\n\nšŸ’” Suggestion: {}", suggestion)?;
184        }
185
186        // Add documentation link
187        if let Some(ref link) = self.doc_link {
188            write!(f, "\nšŸ“– Documentation: {}", link)?;
189        }
190
191        // Add source error
192        if let Some(ref source) = self.source {
193            write!(f, "\n\nCaused by: {}", source)?;
194        }
195
196        Ok(())
197    }
198}
199
200impl std::error::Error for RustLogicGraphError {
201    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
202        self.source
203            .as_ref()
204            .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
205    }
206}
207
208// Convenience constructors for common error types
209
210impl RustLogicGraphError {
211    /// Node execution error
212    pub fn node_execution_error(node_id: impl Into<String>, message: impl Into<String>) -> Self {
213        Self::new("E001", message, ErrorCategory::Retryable)
214            .with_context(ErrorContext::new().with_node(node_id))
215            .with_suggestion(
216                "Check node configuration and input data. Verify all dependencies are available.",
217            )
218    }
219
220    /// Database connection error
221    pub fn database_connection_error(message: impl Into<String>) -> Self {
222        Self::new("E002", message, ErrorCategory::Retryable)
223            .with_suggestion("Verify database connection string, credentials, and network connectivity. Check if database server is running.")
224    }
225
226    /// Rule evaluation error
227    pub fn rule_evaluation_error(message: impl Into<String>) -> Self {
228        Self::new("E003", message, ErrorCategory::Permanent)
229            .with_suggestion("Check rule syntax and ensure all required facts are present. Verify rule logic is correct.")
230    }
231
232    /// Configuration error
233    pub fn configuration_error(message: impl Into<String>) -> Self {
234        Self::new("E004", message, ErrorCategory::Configuration)
235            .with_suggestion("Review configuration file for missing or invalid values. Check against schema documentation.")
236    }
237
238    /// Timeout error
239    pub fn timeout_error(message: impl Into<String>) -> Self {
240        Self::new("E005", message, ErrorCategory::Transient)
241            .with_suggestion("Increase timeout duration or investigate performance bottlenecks. Check for slow downstream services.")
242    }
243
244    /// Graph validation error
245    pub fn graph_validation_error(message: impl Into<String>) -> Self {
246        Self::new("E006", message, ErrorCategory::Permanent)
247            .with_suggestion("Verify graph structure is valid. Check for cycles, missing nodes, or invalid edge connections.")
248    }
249
250    /// Serialization error
251    pub fn serialization_error(message: impl Into<String>) -> Self {
252        Self::new("E007", message, ErrorCategory::Permanent)
253            .with_suggestion("Check data format and ensure all required fields are present. Verify JSON/YAML syntax is valid.")
254    }
255
256    /// AI/LLM error
257    pub fn ai_error(message: impl Into<String>) -> Self {
258        Self::new("E008", message, ErrorCategory::Retryable)
259            .with_suggestion("Verify API key and model availability. Check rate limits and quota. Review prompt for issues.")
260    }
261
262    /// Cache error
263    pub fn cache_error(message: impl Into<String>) -> Self {
264        Self::new("E009", message, ErrorCategory::Transient).with_suggestion(
265            "Check cache configuration and connectivity. Verify cache backend is operational.",
266        )
267    }
268
269    /// Context error
270    pub fn context_error(message: impl Into<String>) -> Self {
271        Self::new("E010", message, ErrorCategory::Permanent)
272            .with_suggestion("Verify context data structure. Ensure required keys are present and values are correct types.")
273    }
274
275    /// Distributed system error
276    pub fn distributed_error(message: impl Into<String>, service: impl Into<String>) -> Self {
277        Self::new("E011", message, ErrorCategory::Retryable)
278            .with_context(ErrorContext::new().with_service(service))
279            .with_suggestion("Check service health and network connectivity. Verify service discovery and load balancing configuration.")
280    }
281
282    /// Transaction coordination error
283    pub fn transaction_error(message: impl Into<String>) -> Self {
284        Self::new("E012", message, ErrorCategory::Transient)
285            .with_suggestion("Review transaction logic and compensation handlers. Check for deadlocks or isolation issues.")
286    }
287}
288
289/// Result type alias for convenience
290pub type Result<T> = std::result::Result<T, RustLogicGraphError>;
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295
296    #[test]
297    fn test_error_creation() {
298        let err = RustLogicGraphError::node_execution_error("node_1", "Failed to execute node");
299        assert_eq!(err.code, "E001");
300        assert_eq!(err.category, ErrorCategory::Retryable);
301        assert!(err.is_retryable());
302        assert!(!err.is_permanent());
303    }
304
305    #[test]
306    fn test_error_with_context() {
307        let context = ErrorContext::new()
308            .with_node("node_1")
309            .with_graph("my_graph")
310            .with_step("execution");
311
312        let err = RustLogicGraphError::new("E001", "Test error", ErrorCategory::Retryable)
313            .with_context(context);
314
315        assert_eq!(err.context.node_id, Some("node_1".to_string()));
316        assert_eq!(err.context.graph_name, Some("my_graph".to_string()));
317    }
318
319    #[test]
320    fn test_error_display() {
321        let err = RustLogicGraphError::database_connection_error("Connection timeout");
322        let display = format!("{}", err);
323
324        assert!(display.contains("[E002]"));
325        assert!(display.contains("Connection timeout"));
326        assert!(display.contains("šŸ’” Suggestion:"));
327        assert!(display.contains("šŸ“– Documentation:"));
328    }
329
330    #[test]
331    fn test_error_categories() {
332        assert!(RustLogicGraphError::database_connection_error("test").is_retryable());
333        assert!(RustLogicGraphError::configuration_error("test").is_permanent());
334        assert!(RustLogicGraphError::timeout_error("test").is_retryable());
335        assert!(RustLogicGraphError::graph_validation_error("test").is_permanent());
336    }
337
338    #[test]
339    fn test_error_with_metadata() {
340        let context = ErrorContext::new()
341            .add_metadata("user_id", "123")
342            .add_metadata("request_id", "req_456");
343
344        let err = RustLogicGraphError::new("E001", "Test", ErrorCategory::Retryable)
345            .with_context(context);
346
347        assert_eq!(err.context.metadata.len(), 2);
348    }
349}