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(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 pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
121 self.suggestion = Some(suggestion.into());
122 self
123 }
124
125 pub fn with_context(mut self, context: ErrorContext) -> Self {
127 self.context = context;
128 self
129 }
130
131 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 pub fn is_retryable(&self) -> bool {
139 matches!(self.category, ErrorCategory::Retryable | ErrorCategory::Transient)
140 }
141
142 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 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 for (key, value) in &self.context.metadata {
168 write!(f, "\n {}: {}", key, value)?;
169 }
170
171 if let Some(ref suggestion) = self.suggestion {
173 write!(f, "\n\nš” Suggestion: {}", suggestion)?;
174 }
175
176 if let Some(ref link) = self.doc_link {
178 write!(f, "\nš Documentation: {}", link)?;
179 }
180
181 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
196impl RustLogicGraphError {
199 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 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 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 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 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 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 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 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 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 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 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 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
274pub 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}