1use std::fmt;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ErrorCategory {
15 Retryable,
17 Permanent,
19 Transient,
21 Configuration,
23}
24
25#[derive(Debug, Clone)]
27pub struct ErrorContext {
28 pub node_id: Option<String>,
30 pub graph_name: Option<String>,
32 pub execution_step: Option<String>,
34 pub service_name: Option<String>,
36 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#[derive(Debug)]
85pub struct RustLogicGraphError {
86 pub code: String,
88 pub message: String,
90 pub category: ErrorCategory,
92 pub suggestion: Option<String>,
94 pub doc_link: Option<String>,
96 pub context: ErrorContext,
98 pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
100}
101
102impl RustLogicGraphError {
103 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 pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
125 self.suggestion = Some(suggestion.into());
126 self
127 }
128
129 pub fn with_context(mut self, context: ErrorContext) -> Self {
131 self.context = context;
132 self
133 }
134
135 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 pub fn is_retryable(&self) -> bool {
143 matches!(
144 self.category,
145 ErrorCategory::Retryable | ErrorCategory::Transient
146 )
147 }
148
149 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 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 for (key, value) in &self.context.metadata {
178 write!(f, "\n {}: {}", key, value)?;
179 }
180
181 if let Some(ref suggestion) = self.suggestion {
183 write!(f, "\n\nš” Suggestion: {}", suggestion)?;
184 }
185
186 if let Some(ref link) = self.doc_link {
188 write!(f, "\nš Documentation: {}", link)?;
189 }
190
191 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
208impl RustLogicGraphError {
211 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 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 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 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 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 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 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 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 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 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 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 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
289pub 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}