1use serde::{Deserialize, Serialize};
11use std::cmp::Reverse;
12use std::fmt;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16pub enum ErrorCategory {
17 Syntax,
19 Validation,
21 Type,
23 FieldNotFound,
25 Argument,
27 Auth,
29 RateLimit,
31 Internal,
33 SparqlTranslation,
35 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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct CodeSnippet {
83 pub source: String,
85 pub start: SourceLocation,
87 pub end: Option<SourceLocation>,
89 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 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct ErrorSuggestion {
159 pub message: String,
161 pub fix: Option<String>,
163 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#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct EnhancedError {
190 pub category: ErrorCategory,
192 pub message: String,
194 pub details: Option<String>,
196 pub location: Option<SourceLocation>,
198 pub snippet: Option<CodeSnippet>,
200 pub suggestions: Vec<ErrorSuggestion>,
202 pub path: Vec<String>,
204 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 pub fn format_user_message(&self) -> String {
253 let mut output = String::new();
254
255 output.push_str(&format!("{}: {}\n", self.category, self.message));
257
258 if let Some(location) = &self.location {
260 output.push_str(&format!(" at {}\n", location));
261 }
262
263 if !self.path.is_empty() {
265 output.push_str(&format!(" in path: {}\n", self.path.join(".")));
266 }
267
268 if let Some(details) = &self.details {
270 output.push_str(&format!("\n{}\n", details));
271 }
272
273 if let Some(snippet) = &self.snippet {
275 output.push('\n');
276 output.push_str(&snippet.format_with_indicator());
277 }
278
279 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 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
343pub struct ErrorBuilder;
345
346impl ErrorBuilder {
347 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 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 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 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 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 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}