Skip to main content

oxirs_gql/
enhanced_errors.rs

1//! Enhanced error messages with better context and suggestions
2//!
3//! This module provides:
4//! - Rich error messages with context
5//! - Source location information
6//! - Suggestions for common errors
7//! - Error categorization
8//! - Stack traces for debugging
9
10use serde::{Deserialize, Serialize};
11use std::cmp::Reverse;
12use std::fmt;
13
14/// Error category for classification
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16pub enum ErrorCategory {
17    /// Syntax error in GraphQL query
18    Syntax,
19    /// Validation error
20    Validation,
21    /// Type error
22    Type,
23    /// Field not found
24    FieldNotFound,
25    /// Argument error
26    Argument,
27    /// Authentication/authorization error
28    Auth,
29    /// Rate limiting error
30    RateLimit,
31    /// Internal server error
32    Internal,
33    /// SPARQL translation error
34    SparqlTranslation,
35    /// Data fetching error
36    DataFetch,
37}
38
39impl fmt::Display for ErrorCategory {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            ErrorCategory::Syntax => write!(f, "Syntax Error"),
43            ErrorCategory::Validation => write!(f, "Validation Error"),
44            ErrorCategory::Type => write!(f, "Type Error"),
45            ErrorCategory::FieldNotFound => write!(f, "Field Not Found"),
46            ErrorCategory::Argument => write!(f, "Argument Error"),
47            ErrorCategory::Auth => write!(f, "Authorization Error"),
48            ErrorCategory::RateLimit => write!(f, "Rate Limit Exceeded"),
49            ErrorCategory::Internal => write!(f, "Internal Server Error"),
50            ErrorCategory::SparqlTranslation => write!(f, "SPARQL Translation Error"),
51            ErrorCategory::DataFetch => write!(f, "Data Fetching Error"),
52        }
53    }
54}
55
56/// Source location in query
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct SourceLocation {
59    pub line: usize,
60    pub column: usize,
61    pub offset: usize,
62}
63
64impl SourceLocation {
65    pub fn new(line: usize, column: usize, offset: usize) -> Self {
66        Self {
67            line,
68            column,
69            offset,
70        }
71    }
72}
73
74impl fmt::Display for SourceLocation {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        write!(f, "line {}, column {}", self.line, self.column)
77    }
78}
79
80/// Code snippet for error context
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct CodeSnippet {
83    /// The source code
84    pub source: String,
85    /// Start location
86    pub start: SourceLocation,
87    /// End location (optional)
88    pub end: Option<SourceLocation>,
89    /// Highlighted sections
90    pub highlights: Vec<(usize, usize)>,
91}
92
93impl CodeSnippet {
94    pub fn new(source: String, start: SourceLocation) -> Self {
95        Self {
96            source,
97            start,
98            end: None,
99            highlights: Vec::new(),
100        }
101    }
102
103    pub fn with_end(mut self, end: SourceLocation) -> Self {
104        self.end = Some(end);
105        self
106    }
107
108    pub fn with_highlight(mut self, start: usize, end: usize) -> Self {
109        self.highlights.push((start, end));
110        self
111    }
112
113    /// Format the code snippet with error indicators
114    pub fn format_with_indicator(&self) -> String {
115        let lines: Vec<&str> = self.source.lines().collect();
116        let mut output = String::new();
117
118        let start_line = self.start.line.saturating_sub(1);
119        let end_line = self.end.as_ref().map(|e| e.line).unwrap_or(self.start.line);
120
121        // Show context (2 lines before and after)
122        let context_start = start_line.saturating_sub(2);
123        let context_end = (end_line + 2).min(lines.len());
124
125        for (idx, line) in lines
126            .iter()
127            .enumerate()
128            .skip(context_start)
129            .take(context_end - context_start)
130        {
131            let line_num = idx + 1;
132            output.push_str(&format!("{:4} | {}\n", line_num, line));
133
134            // Add indicator for error line
135            if line_num == self.start.line {
136                output.push_str("     | ");
137                for _ in 0..self.start.column.saturating_sub(1) {
138                    output.push(' ');
139                }
140                output.push('^');
141                if let Some(end) = &self.end {
142                    if end.line == self.start.line {
143                        for _ in self.start.column..end.column.saturating_sub(1) {
144                            output.push('~');
145                        }
146                    }
147                }
148                output.push('\n');
149            }
150        }
151
152        output
153    }
154}
155
156/// Error suggestion
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct ErrorSuggestion {
159    /// Suggestion message
160    pub message: String,
161    /// Optional fix (replacement text)
162    pub fix: Option<String>,
163    /// Priority (higher = more likely to be correct)
164    pub priority: u32,
165}
166
167impl ErrorSuggestion {
168    pub fn new(message: String) -> Self {
169        Self {
170            message,
171            fix: None,
172            priority: 50,
173        }
174    }
175
176    pub fn with_fix(mut self, fix: String) -> Self {
177        self.fix = Some(fix);
178        self
179    }
180
181    pub fn with_priority(mut self, priority: u32) -> Self {
182        self.priority = priority;
183        self
184    }
185}
186
187/// Enhanced GraphQL error with rich context
188#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct EnhancedError {
190    /// Error category
191    pub category: ErrorCategory,
192    /// Primary error message
193    pub message: String,
194    /// Detailed explanation
195    pub details: Option<String>,
196    /// Source location
197    pub location: Option<SourceLocation>,
198    /// Code snippet
199    pub snippet: Option<CodeSnippet>,
200    /// Suggestions for fixing
201    pub suggestions: Vec<ErrorSuggestion>,
202    /// Path to the field that caused error
203    pub path: Vec<String>,
204    /// Extensions (additional metadata)
205    pub extensions: serde_json::Map<String, serde_json::Value>,
206}
207
208impl EnhancedError {
209    pub fn new(category: ErrorCategory, message: String) -> Self {
210        Self {
211            category,
212            message,
213            details: None,
214            location: None,
215            snippet: None,
216            suggestions: Vec::new(),
217            path: Vec::new(),
218            extensions: serde_json::Map::new(),
219        }
220    }
221
222    pub fn with_details(mut self, details: String) -> Self {
223        self.details = Some(details);
224        self
225    }
226
227    pub fn with_location(mut self, location: SourceLocation) -> Self {
228        self.location = Some(location);
229        self
230    }
231
232    pub fn with_snippet(mut self, snippet: CodeSnippet) -> Self {
233        self.snippet = Some(snippet);
234        self
235    }
236
237    pub fn with_suggestion(mut self, suggestion: ErrorSuggestion) -> Self {
238        self.suggestions.push(suggestion);
239        self
240    }
241
242    pub fn with_path(mut self, path: Vec<String>) -> Self {
243        self.path = path;
244        self
245    }
246
247    pub fn add_extension(&mut self, key: String, value: serde_json::Value) {
248        self.extensions.insert(key, value);
249    }
250
251    /// Format error as a user-friendly message
252    pub fn format_user_message(&self) -> String {
253        let mut output = String::new();
254
255        // Category and message
256        output.push_str(&format!("{}: {}\n", self.category, self.message));
257
258        // Location
259        if let Some(location) = &self.location {
260            output.push_str(&format!("  at {}\n", location));
261        }
262
263        // Path
264        if !self.path.is_empty() {
265            output.push_str(&format!("  in path: {}\n", self.path.join(".")));
266        }
267
268        // Details
269        if let Some(details) = &self.details {
270            output.push_str(&format!("\n{}\n", details));
271        }
272
273        // Code snippet
274        if let Some(snippet) = &self.snippet {
275            output.push('\n');
276            output.push_str(&snippet.format_with_indicator());
277        }
278
279        // Suggestions
280        if !self.suggestions.is_empty() {
281            output.push_str("\nSuggestions:\n");
282            let mut sorted_suggestions = self.suggestions.clone();
283            sorted_suggestions.sort_by_key(|s| Reverse(s.priority));
284
285            for (idx, suggestion) in sorted_suggestions.iter().enumerate().take(3) {
286                output.push_str(&format!("  {}. {}\n", idx + 1, suggestion.message));
287                if let Some(fix) = &suggestion.fix {
288                    output.push_str(&format!("     Try: {}\n", fix));
289                }
290            }
291        }
292
293        output
294    }
295
296    /// Convert to GraphQL error format
297    pub fn to_graphql_error(&self) -> serde_json::Value {
298        let mut error = serde_json::json!({
299            "message": self.message,
300        });
301
302        if let Some(location) = &self.location {
303            error["locations"] = serde_json::json!([{
304                "line": location.line,
305                "column": location.column,
306            }]);
307        }
308
309        if !self.path.is_empty() {
310            error["path"] = serde_json::json!(self.path);
311        }
312
313        let mut extensions = self.extensions.clone();
314        extensions.insert(
315            "category".to_string(),
316            serde_json::json!(format!("{:?}", self.category)),
317        );
318
319        if let Some(details) = &self.details {
320            extensions.insert("details".to_string(), serde_json::json!(details));
321        }
322
323        if !self.suggestions.is_empty() {
324            let suggestions: Vec<String> =
325                self.suggestions.iter().map(|s| s.message.clone()).collect();
326            extensions.insert("suggestions".to_string(), serde_json::json!(suggestions));
327        }
328
329        error["extensions"] = serde_json::json!(extensions);
330
331        error
332    }
333}
334
335impl fmt::Display for EnhancedError {
336    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
337        write!(f, "{}", self.format_user_message())
338    }
339}
340
341impl std::error::Error for EnhancedError {}
342
343/// Common error builders
344pub struct ErrorBuilder;
345
346impl ErrorBuilder {
347    /// Field not found error
348    pub fn field_not_found(type_name: &str, field_name: &str) -> EnhancedError {
349        EnhancedError::new(
350            ErrorCategory::FieldNotFound,
351            format!("Field '{}' not found on type '{}'", field_name, type_name),
352        )
353        .with_details(format!(
354            "The type '{}' does not have a field named '{}'.",
355            type_name, field_name
356        ))
357        .with_suggestion(
358            ErrorSuggestion::new(format!(
359                "Check the schema documentation for available fields on type '{}'",
360                type_name
361            ))
362            .with_priority(90),
363        )
364    }
365
366    /// Type mismatch error
367    pub fn type_mismatch(expected: &str, actual: &str, field: &str) -> EnhancedError {
368        EnhancedError::new(
369            ErrorCategory::Type,
370            format!(
371                "Type mismatch for field '{}': expected '{}', got '{}'",
372                field, expected, actual
373            ),
374        )
375        .with_details(format!(
376            "The field '{}' expects values of type '{}', but received '{}'.",
377            field, expected, actual
378        ))
379        .with_suggestion(
380            ErrorSuggestion::new(format!("Ensure the value is of type '{}'", expected))
381                .with_priority(95),
382        )
383    }
384
385    /// Missing required argument
386    pub fn missing_argument(field: &str, argument: &str) -> EnhancedError {
387        EnhancedError::new(
388            ErrorCategory::Argument,
389            format!(
390                "Missing required argument '{}' for field '{}'",
391                argument, field
392            ),
393        )
394        .with_details(format!(
395            "The field '{}' requires an argument '{}' to be provided.",
396            field, argument
397        ))
398        .with_suggestion(
399            ErrorSuggestion::new(format!(
400                "Add the '{}' argument to the field '{}'",
401                argument, field
402            ))
403            .with_fix(format!("{}({}: <value>)", field, argument))
404            .with_priority(100),
405        )
406    }
407
408    /// Syntax error
409    pub fn syntax_error(message: String, location: SourceLocation) -> EnhancedError {
410        EnhancedError::new(ErrorCategory::Syntax, message)
411            .with_location(location)
412            .with_suggestion(
413                ErrorSuggestion::new("Check the GraphQL query syntax".to_string())
414                    .with_priority(80),
415            )
416    }
417
418    /// Rate limit exceeded
419    pub fn rate_limit_exceeded(limit: u32, window: u64) -> EnhancedError {
420        EnhancedError::new(
421            ErrorCategory::RateLimit,
422            format!(
423                "Rate limit of {} requests per {} seconds exceeded",
424                limit, window
425            ),
426        )
427        .with_details(
428            "You have made too many requests. Please wait before trying again.".to_string(),
429        )
430        .with_suggestion(
431            ErrorSuggestion::new(format!(
432                "Wait {} seconds before making another request",
433                window
434            ))
435            .with_priority(100),
436        )
437    }
438
439    /// Internal server error
440    pub fn internal_error(message: String) -> EnhancedError {
441        EnhancedError::new(ErrorCategory::Internal, "Internal server error".to_string())
442            .with_details(message)
443            .with_suggestion(
444                ErrorSuggestion::new(
445                    "This is an internal error. Please contact support if it persists.".to_string(),
446                )
447                .with_priority(50),
448            )
449    }
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455
456    #[test]
457    fn test_error_category_display() {
458        assert_eq!(ErrorCategory::Syntax.to_string(), "Syntax Error");
459        assert_eq!(ErrorCategory::Validation.to_string(), "Validation Error");
460        assert_eq!(ErrorCategory::Type.to_string(), "Type Error");
461    }
462
463    #[test]
464    fn test_source_location() {
465        let loc = SourceLocation::new(5, 10, 42);
466        assert_eq!(loc.line, 5);
467        assert_eq!(loc.column, 10);
468        assert_eq!(loc.offset, 42);
469        assert_eq!(loc.to_string(), "line 5, column 10");
470    }
471
472    #[test]
473    fn test_error_suggestion() {
474        let suggestion = ErrorSuggestion::new("Check the field name".to_string())
475            .with_fix("Use 'name' instead of 'names'".to_string())
476            .with_priority(90);
477
478        assert_eq!(suggestion.message, "Check the field name");
479        assert!(suggestion.fix.is_some());
480        assert_eq!(suggestion.priority, 90);
481    }
482
483    #[test]
484    fn test_enhanced_error_creation() {
485        let error = EnhancedError::new(ErrorCategory::FieldNotFound, "Field not found".to_string())
486            .with_details("The field 'foo' does not exist".to_string())
487            .with_path(vec!["query".to_string(), "user".to_string()]);
488
489        assert_eq!(error.category, ErrorCategory::FieldNotFound);
490        assert_eq!(error.message, "Field not found");
491        assert_eq!(error.path.len(), 2);
492    }
493
494    #[test]
495    fn test_error_builder_field_not_found() {
496        let error = ErrorBuilder::field_not_found("User", "email");
497
498        assert_eq!(error.category, ErrorCategory::FieldNotFound);
499        assert!(error.message.contains("email"));
500        assert!(error.message.contains("User"));
501        assert_eq!(error.suggestions.len(), 1);
502    }
503
504    #[test]
505    fn test_error_builder_type_mismatch() {
506        let error = ErrorBuilder::type_mismatch("String", "Int", "name");
507
508        assert_eq!(error.category, ErrorCategory::Type);
509        assert!(error.message.contains("String"));
510        assert!(error.message.contains("Int"));
511    }
512
513    #[test]
514    fn test_error_builder_missing_argument() {
515        let error = ErrorBuilder::missing_argument("getUser", "id");
516
517        assert_eq!(error.category, ErrorCategory::Argument);
518        assert!(error.message.contains("id"));
519        assert!(error.message.contains("getUser"));
520        assert!(error.suggestions[0].fix.is_some());
521    }
522
523    #[test]
524    fn test_error_to_graphql() {
525        let error = EnhancedError::new(ErrorCategory::Syntax, "Parse error".to_string())
526            .with_location(SourceLocation::new(1, 5, 4))
527            .with_path(vec!["query".to_string()]);
528
529        let graphql_error = error.to_graphql_error();
530
531        assert_eq!(graphql_error["message"], "Parse error");
532        assert!(graphql_error["locations"].is_array());
533        assert!(graphql_error["path"].is_array());
534        assert!(graphql_error["extensions"].is_object());
535    }
536
537    #[test]
538    fn test_code_snippet_formatting() {
539        let source = "query {\n  user {\n    naame\n  }\n}".to_string();
540        let snippet = CodeSnippet::new(source, SourceLocation::new(3, 5, 0));
541
542        let formatted = snippet.format_with_indicator();
543        assert!(formatted.contains("naame"));
544        assert!(formatted.contains("^"));
545    }
546
547    #[test]
548    fn test_error_format_user_message() {
549        let error = EnhancedError::new(
550            ErrorCategory::FieldNotFound,
551            "Field 'email' not found".to_string(),
552        )
553        .with_suggestion(ErrorSuggestion::new("Check spelling".to_string()));
554
555        let message = error.format_user_message();
556        assert!(message.contains("Field Not Found"));
557        assert!(message.contains("email"));
558        assert!(message.contains("Suggestions"));
559    }
560
561    #[test]
562    fn test_error_builder_rate_limit() {
563        let error = ErrorBuilder::rate_limit_exceeded(100, 60);
564
565        assert_eq!(error.category, ErrorCategory::RateLimit);
566        assert!(error.message.contains("100"));
567        assert!(error.message.contains("60"));
568    }
569
570    #[test]
571    fn test_error_builder_internal() {
572        let error = ErrorBuilder::internal_error("Database connection failed".to_string());
573
574        assert_eq!(error.category, ErrorCategory::Internal);
575        assert!(error.details.is_some());
576    }
577}