Skip to main content

oxirs_star/
enhanced_errors.rs

1//! Enhanced Error Handling for RDF-star
2//!
3//! This module provides improved error message quality, structured error reporting,
4//! and enhanced context preservation for RDF-star operations.
5
6use crate::{StarError, StarResult};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::fmt;
10
11/// Enhanced error context with detailed information
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ErrorContext {
14    /// Source file or input identifier
15    pub source: Option<String>,
16    /// Line number where error occurred
17    pub line: Option<usize>,
18    /// Column number where error occurred
19    pub column: Option<usize>,
20    /// Surrounding text for context
21    pub snippet: Option<String>,
22    /// Operation being performed
23    pub operation: Option<String>,
24    /// Additional metadata
25    pub metadata: HashMap<String, String>,
26}
27
28impl ErrorContext {
29    /// Create a new error context
30    pub fn new() -> Self {
31        Self {
32            source: None,
33            line: None,
34            column: None,
35            snippet: None,
36            operation: None,
37            metadata: HashMap::new(),
38        }
39    }
40
41    /// Set the source file or identifier
42    pub fn with_source(mut self, source: impl Into<String>) -> Self {
43        self.source = Some(source.into());
44        self
45    }
46
47    /// Set the line and column position
48    pub fn with_position(mut self, line: usize, column: usize) -> Self {
49        self.line = Some(line);
50        self.column = Some(column);
51        self
52    }
53
54    /// Set a code snippet for context
55    pub fn with_snippet(mut self, snippet: impl Into<String>) -> Self {
56        self.snippet = Some(snippet.into());
57        self
58    }
59
60    /// Set the operation being performed
61    pub fn with_operation(mut self, operation: impl Into<String>) -> Self {
62        self.operation = Some(operation.into());
63        self
64    }
65
66    /// Add metadata key-value pair
67    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
68        self.metadata.insert(key.into(), value.into());
69        self
70    }
71}
72
73impl Default for ErrorContext {
74    fn default() -> Self {
75        Self::new()
76    }
77}
78
79/// Enhanced error with rich context and formatting
80#[derive(Debug)]
81pub struct EnhancedError {
82    /// The underlying StarError
83    pub error: StarError,
84    /// Enhanced context information
85    pub context: Box<ErrorContext>,
86    /// Severity level
87    pub severity: ErrorSeverity,
88    /// Error category for grouping
89    pub category: ErrorCategory,
90    /// Recovery suggestions
91    pub suggestions: Box<Vec<String>>,
92}
93
94/// Error severity levels
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
96pub enum ErrorSeverity {
97    /// Critical errors that prevent operation
98    Critical,
99    /// Errors that cause operation failure
100    Error,
101    /// Warnings that indicate potential issues
102    Warning,
103    /// Informational messages
104    Info,
105}
106
107/// Error categories for grouping and handling
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
109pub enum ErrorCategory {
110    /// Syntax and parsing errors
111    Syntax,
112    /// Semantic validation errors
113    Semantic,
114    /// Configuration and setup errors
115    Configuration,
116    /// Runtime and execution errors
117    Runtime,
118    /// I/O and resource errors
119    IO,
120    /// Network and connectivity errors
121    Network,
122}
123
124impl EnhancedError {
125    /// Create a new enhanced error
126    pub fn new(error: StarError) -> Self {
127        let (severity, category) = Self::classify_error(&error);
128        Self {
129            error,
130            context: Box::new(ErrorContext::new()),
131            severity,
132            category,
133            suggestions: Box::new(Vec::new()),
134        }
135    }
136
137    /// Add context to the error
138    pub fn with_context(mut self, context: ErrorContext) -> Self {
139        self.context = Box::new(context);
140        self
141    }
142
143    /// Set the severity level
144    pub fn with_severity(mut self, severity: ErrorSeverity) -> Self {
145        self.severity = severity;
146        self
147    }
148
149    /// Add a recovery suggestion
150    pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
151        self.suggestions.push(suggestion.into());
152        self
153    }
154
155    /// Add multiple recovery suggestions
156    pub fn with_suggestions(mut self, suggestions: Vec<String>) -> Self {
157        self.suggestions.extend(suggestions);
158        self
159    }
160
161    /// Classify error to determine severity and category
162    fn classify_error(error: &StarError) -> (ErrorSeverity, ErrorCategory) {
163        match error {
164            StarError::InvalidQuotedTriple { .. } => {
165                (ErrorSeverity::Error, ErrorCategory::Semantic)
166            }
167            StarError::ParseError(_) => (ErrorSeverity::Error, ErrorCategory::Syntax),
168            StarError::SerializationError { .. } => (ErrorSeverity::Error, ErrorCategory::Runtime),
169            StarError::QueryError { .. } => (ErrorSeverity::Error, ErrorCategory::Syntax),
170            StarError::CoreError(_) => (ErrorSeverity::Error, ErrorCategory::Runtime),
171            StarError::ReificationError { .. } => (ErrorSeverity::Error, ErrorCategory::Semantic),
172            StarError::InvalidTermType { .. } => (ErrorSeverity::Error, ErrorCategory::Semantic),
173            StarError::NestingDepthExceeded { .. } => {
174                (ErrorSeverity::Error, ErrorCategory::Configuration)
175            }
176            StarError::UnsupportedFormat { .. } => {
177                (ErrorSeverity::Error, ErrorCategory::Configuration)
178            }
179            StarError::ConfigurationError { .. } => {
180                (ErrorSeverity::Error, ErrorCategory::Configuration)
181            }
182            StarError::InternalError { .. } => (ErrorSeverity::Critical, ErrorCategory::Runtime),
183        }
184    }
185
186    /// Generate enhanced error message with context
187    pub fn formatted_message(&self) -> String {
188        let mut message = format!("[{}] {}", self.severity_label(), self.error);
189
190        // Add context information
191        if let Some(source) = &self.context.source {
192            message.push_str(&format!("\n  Source: {source}"));
193        }
194
195        if let (Some(line), Some(column)) = (self.context.line, self.context.column) {
196            message.push_str(&format!("\n  Location: line {line}, column {column}"));
197        }
198
199        if let Some(operation) = &self.context.operation {
200            message.push_str(&format!("\n  Operation: {operation}"));
201        }
202
203        if let Some(snippet) = &self.context.snippet {
204            message.push_str(&format!(
205                "\n  Context:\n    {}",
206                snippet.replace('\n', "\n    ")
207            ));
208        }
209
210        // Add metadata
211        if !self.context.metadata.is_empty() {
212            message.push_str("\n  Details:");
213            for (key, value) in &self.context.metadata {
214                message.push_str(&format!("\n    {key}: {value}"));
215            }
216        }
217
218        // Add suggestions
219        if !self.suggestions.is_empty() {
220            message.push_str("\n  Suggestions:");
221            for (i, suggestion) in self.suggestions.iter().enumerate() {
222                message.push_str(&format!("\n    {}. {suggestion}", i + 1));
223            }
224        }
225
226        message
227    }
228
229    /// Get severity label
230    fn severity_label(&self) -> &'static str {
231        match self.severity {
232            ErrorSeverity::Critical => "CRITICAL",
233            ErrorSeverity::Error => "ERROR",
234            ErrorSeverity::Warning => "WARNING",
235            ErrorSeverity::Info => "INFO",
236        }
237    }
238
239    /// Convert to JSON for structured reporting
240    pub fn to_json(&self) -> serde_json::Value {
241        serde_json::json!({
242            "severity": self.severity,
243            "category": self.category,
244            "message": self.error.to_string(),
245            "context": {
246                "source": self.context.source,
247                "line": self.context.line,
248                "column": self.context.column,
249                "snippet": self.context.snippet,
250                "operation": self.context.operation,
251                "metadata": self.context.metadata
252            },
253            "suggestions": self.suggestions
254        })
255    }
256}
257
258impl fmt::Display for EnhancedError {
259    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260        write!(f, "{}", self.formatted_message())
261    }
262}
263
264/// Error aggregator for collecting and reporting multiple errors
265#[derive(Debug, Default)]
266pub struct ErrorAggregator {
267    errors: Vec<EnhancedError>,
268    warnings: Vec<EnhancedError>,
269    max_errors: Option<usize>,
270    max_warnings: Option<usize>,
271}
272
273impl ErrorAggregator {
274    /// Create a new error aggregator
275    pub fn new() -> Self {
276        Self {
277            errors: Vec::new(),
278            warnings: Vec::new(),
279            max_errors: None,
280            max_warnings: None,
281        }
282    }
283
284    /// Set maximum number of errors to collect
285    pub fn with_max_errors(mut self, max: usize) -> Self {
286        self.max_errors = Some(max);
287        self
288    }
289
290    /// Set maximum number of warnings to collect
291    pub fn with_max_warnings(mut self, max: usize) -> Self {
292        self.max_warnings = Some(max);
293        self
294    }
295
296    /// Add an enhanced error
297    pub fn add_error(&mut self, error: EnhancedError) {
298        match error.severity {
299            ErrorSeverity::Critical | ErrorSeverity::Error => {
300                if let Some(max) = self.max_errors {
301                    if self.errors.len() >= max {
302                        return;
303                    }
304                }
305                self.errors.push(error);
306            }
307            ErrorSeverity::Warning | ErrorSeverity::Info => {
308                if let Some(max) = self.max_warnings {
309                    if self.warnings.len() >= max {
310                        return;
311                    }
312                }
313                self.warnings.push(error);
314            }
315        }
316    }
317
318    /// Add a StarError with automatic enhancement
319    pub fn add_star_error(&mut self, error: StarError, context: Option<ErrorContext>) {
320        let enhanced = if let Some(ctx) = context {
321            EnhancedError::new(error).with_context(ctx)
322        } else {
323            EnhancedError::new(error)
324        };
325        self.add_error(enhanced);
326    }
327
328    /// Check if there are any errors
329    pub fn has_errors(&self) -> bool {
330        !self.errors.is_empty()
331    }
332
333    /// Check if there are any warnings
334    pub fn has_warnings(&self) -> bool {
335        !self.warnings.is_empty()
336    }
337
338    /// Get error count
339    pub fn error_count(&self) -> usize {
340        self.errors.len()
341    }
342
343    /// Get warning count
344    pub fn warning_count(&self) -> usize {
345        self.warnings.len()
346    }
347
348    /// Get all errors
349    pub fn errors(&self) -> &[EnhancedError] {
350        &self.errors
351    }
352
353    /// Get all warnings
354    pub fn warnings(&self) -> &[EnhancedError] {
355        &self.warnings
356    }
357
358    /// Generate a comprehensive error report
359    pub fn generate_report(&self) -> String {
360        let mut report = String::new();
361
362        // Summary
363        report.push_str(&format!(
364            "Error Report: {} error(s), {} warning(s)\n",
365            self.errors.len(),
366            self.warnings.len()
367        ));
368        report.push_str("═".repeat(50).as_str());
369        report.push('\n');
370
371        // Errors
372        if !self.errors.is_empty() {
373            report.push_str("\nERRORS:\n");
374            for (i, error) in self.errors.iter().enumerate() {
375                report.push_str(&format!("\n{}. {}\n", i + 1, error.formatted_message()));
376            }
377        }
378
379        // Warnings
380        if !self.warnings.is_empty() {
381            report.push_str("\nWARNINGS:\n");
382            for (i, warning) in self.warnings.iter().enumerate() {
383                report.push_str(&format!("\n{}. {}\n", i + 1, warning.formatted_message()));
384            }
385        }
386
387        report
388    }
389
390    /// Generate structured JSON report
391    pub fn generate_json_report(&self) -> serde_json::Value {
392        serde_json::json!({
393            "summary": {
394                "error_count": self.errors.len(),
395                "warning_count": self.warnings.len(),
396                "has_errors": self.has_errors()
397            },
398            "errors": self.errors.iter().map(|e| e.to_json()).collect::<Vec<_>>(),
399            "warnings": self.warnings.iter().map(|w| w.to_json()).collect::<Vec<_>>()
400        })
401    }
402
403    /// Clear all collected errors and warnings
404    pub fn clear(&mut self) {
405        self.errors.clear();
406        self.warnings.clear();
407    }
408}
409
410/// Enhanced result type with error context
411pub type EnhancedResult<T> = Result<T, EnhancedError>;
412
413/// Trait for converting StarError to EnhancedError with context
414pub trait WithErrorContext<T> {
415    /// Add context to an error result
416    fn with_context(self, context: ErrorContext) -> EnhancedResult<T>;
417
418    /// Add context and suggestions to an error result
419    fn with_context_and_suggestions(
420        self,
421        context: ErrorContext,
422        suggestions: Vec<String>,
423    ) -> EnhancedResult<T>;
424}
425
426impl<T> WithErrorContext<T> for StarResult<T> {
427    fn with_context(self, context: ErrorContext) -> EnhancedResult<T> {
428        self.map_err(|e| EnhancedError::new(e).with_context(context))
429    }
430
431    fn with_context_and_suggestions(
432        self,
433        context: ErrorContext,
434        suggestions: Vec<String>,
435    ) -> EnhancedResult<T> {
436        self.map_err(|e| {
437            EnhancedError::new(e)
438                .with_context(context)
439                .with_suggestions(suggestions)
440        })
441    }
442}
443
444#[cfg(test)]
445mod tests {
446    use super::*;
447
448    #[test]
449    fn test_error_context_builder() {
450        let context = ErrorContext::new()
451            .with_source("test.ttls")
452            .with_position(10, 5)
453            .with_snippet("<< ex:alice ex:knows ex:bob >> ex:certainty 0.9 .")
454            .with_operation("parsing")
455            .with_metadata("format", "turtle-star");
456
457        assert_eq!(context.source, Some("test.ttls".to_string()));
458        assert_eq!(context.line, Some(10));
459        assert_eq!(context.column, Some(5));
460        assert!(context.snippet.is_some());
461        assert_eq!(context.operation, Some("parsing".to_string()));
462        assert_eq!(
463            context.metadata.get("format"),
464            Some(&"turtle-star".to_string())
465        );
466    }
467
468    #[test]
469    fn test_enhanced_error_classification() {
470        let parse_error = StarError::parse_error("Invalid syntax");
471        let enhanced = EnhancedError::new(parse_error);
472
473        assert_eq!(enhanced.severity, ErrorSeverity::Error);
474        assert_eq!(enhanced.category, ErrorCategory::Syntax);
475    }
476
477    #[test]
478    fn test_error_aggregator() {
479        let mut aggregator = ErrorAggregator::new().with_max_errors(2);
480
481        aggregator.add_star_error(StarError::parse_error("Error 1"), None);
482        aggregator.add_star_error(StarError::parse_error("Error 2"), None);
483        aggregator.add_star_error(StarError::parse_error("Error 3"), None); // Should be ignored
484
485        assert_eq!(aggregator.error_count(), 2);
486        assert!(aggregator.has_errors());
487    }
488
489    #[test]
490    fn test_enhanced_error_formatting() {
491        let context = ErrorContext::new()
492            .with_source("test.ttls")
493            .with_position(5, 10);
494
495        let enhanced = EnhancedError::new(StarError::parse_error("Invalid token"))
496            .with_context(context)
497            .with_suggestion("Check syntax around line 5");
498
499        let message = enhanced.formatted_message();
500        assert!(message.contains("ERROR"));
501        assert!(message.contains("test.ttls"));
502        assert!(message.contains("line 5, column 10"));
503        assert!(message.contains("Check syntax"));
504    }
505
506    #[test]
507    fn test_json_report_generation() {
508        let mut aggregator = ErrorAggregator::new();
509        aggregator.add_star_error(StarError::parse_error("Test error"), None);
510
511        let report = aggregator.generate_json_report();
512        assert_eq!(report["summary"]["error_count"], 1);
513        assert!(report["errors"].is_array());
514    }
515}