1use std::collections::HashMap;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum ErrorClass {
16 Validation,
18 Execution,
20 Internal,
22 Authorization,
24 RateLimit,
26 InputCoercion,
28}
29
30impl ErrorClass {
31 pub fn code(&self) -> &'static str {
33 match self {
34 ErrorClass::Validation => "VALIDATION_ERROR",
35 ErrorClass::Execution => "EXECUTION_ERROR",
36 ErrorClass::Internal => "INTERNAL_ERROR",
37 ErrorClass::Authorization => "AUTHORIZATION_ERROR",
38 ErrorClass::RateLimit => "RATE_LIMIT_ERROR",
39 ErrorClass::InputCoercion => "INPUT_COERCION_ERROR",
40 }
41 }
42
43 pub fn label(&self) -> &'static str {
45 match self {
46 ErrorClass::Validation => "Validation Error",
47 ErrorClass::Execution => "Execution Error",
48 ErrorClass::Internal => "Internal Error",
49 ErrorClass::Authorization => "Authorization Error",
50 ErrorClass::RateLimit => "Rate Limit Error",
51 ErrorClass::InputCoercion => "Input Coercion Error",
52 }
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
62pub enum Severity {
63 Info,
65 Warning,
67 Error,
69 Critical,
71}
72
73impl Severity {
74 pub fn label(&self) -> &'static str {
75 match self {
76 Severity::Info => "info",
77 Severity::Warning => "warning",
78 Severity::Error => "error",
79 Severity::Critical => "critical",
80 }
81 }
82
83 pub fn from_class(class: ErrorClass) -> Self {
85 match class {
86 ErrorClass::Validation => Severity::Error,
87 ErrorClass::Execution => Severity::Error,
88 ErrorClass::Internal => Severity::Critical,
89 ErrorClass::Authorization => Severity::Error,
90 ErrorClass::RateLimit => Severity::Warning,
91 ErrorClass::InputCoercion => Severity::Error,
92 }
93 }
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Hash)]
102pub enum PathSegment {
103 Field(String),
105 Index(usize),
107}
108
109impl PathSegment {
110 pub fn field(name: impl Into<String>) -> Self {
111 PathSegment::Field(name.into())
112 }
113
114 pub fn index(i: usize) -> Self {
115 PathSegment::Index(i)
116 }
117}
118
119impl std::fmt::Display for PathSegment {
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 match self {
122 PathSegment::Field(name) => write!(f, "{}", name),
123 PathSegment::Index(i) => write!(f, "{}", i),
124 }
125 }
126}
127
128pub fn format_path(path: &[PathSegment]) -> String {
130 path.iter()
131 .map(|s| s.to_string())
132 .collect::<Vec<_>>()
133 .join(".")
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct ErrorExtensions {
143 pub code: String,
145 pub timestamp: Option<String>,
147 pub trace_id: Option<String>,
149 pub extra: HashMap<String, String>,
151}
152
153impl ErrorExtensions {
154 pub fn new(code: impl Into<String>) -> Self {
155 Self {
156 code: code.into(),
157 timestamp: None,
158 trace_id: None,
159 extra: HashMap::new(),
160 }
161 }
162
163 pub fn with_timestamp(mut self, ts: impl Into<String>) -> Self {
164 self.timestamp = Some(ts.into());
165 self
166 }
167
168 pub fn with_trace_id(mut self, id: impl Into<String>) -> Self {
169 self.trace_id = Some(id.into());
170 self
171 }
172
173 pub fn with_extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
174 self.extra.insert(key.into(), value.into());
175 self
176 }
177}
178
179#[derive(Debug, Clone)]
185pub struct GraphQLError {
186 pub message: String,
188 pub classification: ErrorClass,
190 pub severity: Severity,
192 pub path: Vec<PathSegment>,
194 pub locations: Vec<SourceLocation>,
196 pub extensions: Option<ErrorExtensions>,
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Hash)]
202pub struct SourceLocation {
203 pub line: usize,
204 pub column: usize,
205}
206
207impl SourceLocation {
208 pub fn new(line: usize, column: usize) -> Self {
209 Self { line, column }
210 }
211}
212
213impl GraphQLError {
214 pub fn new(message: impl Into<String>, classification: ErrorClass) -> Self {
216 Self {
217 message: message.into(),
218 classification,
219 severity: Severity::from_class(classification),
220 path: Vec::new(),
221 locations: Vec::new(),
222 extensions: None,
223 }
224 }
225
226 pub fn with_path(mut self, path: Vec<PathSegment>) -> Self {
228 self.path = path;
229 self
230 }
231
232 pub fn with_locations(mut self, locations: Vec<SourceLocation>) -> Self {
234 self.locations = locations;
235 self
236 }
237
238 pub fn with_severity(mut self, severity: Severity) -> Self {
240 self.severity = severity;
241 self
242 }
243
244 pub fn with_extensions(mut self, ext: ErrorExtensions) -> Self {
246 self.extensions = Some(ext);
247 self
248 }
249
250 pub fn with_code(self, code: impl Into<String>) -> Self {
252 self.with_extensions(ErrorExtensions::new(code))
253 }
254
255 pub fn path_string(&self) -> String {
257 format_path(&self.path)
258 }
259
260 pub fn dedup_key(&self) -> String {
262 format!(
263 "{}:{}:{}",
264 self.classification.code(),
265 self.path_string(),
266 self.message
267 )
268 }
269}
270
271pub struct ErrorMessageRegistry {
277 messages: HashMap<String, String>,
279}
280
281impl Default for ErrorMessageRegistry {
282 fn default() -> Self {
283 Self::new()
284 }
285}
286
287impl ErrorMessageRegistry {
288 pub fn new() -> Self {
289 Self {
290 messages: HashMap::new(),
291 }
292 }
293
294 pub fn register(&mut self, code: impl Into<String>, message: impl Into<String>) {
296 self.messages.insert(code.into(), message.into());
297 }
298
299 pub fn get(&self, code: &str) -> Option<&str> {
301 self.messages.get(code).map(|s| s.as_str())
302 }
303
304 pub fn apply(&self, error: &mut GraphQLError) {
307 if let Some(ext) = &error.extensions {
308 if let Some(custom) = self.messages.get(&ext.code) {
309 error.message = custom.clone();
310 }
311 }
312 }
313
314 pub fn len(&self) -> usize {
316 self.messages.len()
317 }
318
319 pub fn is_empty(&self) -> bool {
320 self.messages.is_empty()
321 }
322}
323
324pub struct ErrorAggregator {
330 errors: HashMap<String, AggregatedError>,
332 order: Vec<String>,
334}
335
336#[derive(Debug, Clone)]
338pub struct AggregatedError {
339 pub error: GraphQLError,
340 pub count: usize,
341}
342
343impl Default for ErrorAggregator {
344 fn default() -> Self {
345 Self::new()
346 }
347}
348
349impl ErrorAggregator {
350 pub fn new() -> Self {
351 Self {
352 errors: HashMap::new(),
353 order: Vec::new(),
354 }
355 }
356
357 pub fn add(&mut self, error: GraphQLError) {
359 let key = error.dedup_key();
360 if let Some(existing) = self.errors.get_mut(&key) {
361 existing.count += 1;
362 } else {
363 self.order.push(key.clone());
364 self.errors.insert(key, AggregatedError { error, count: 1 });
365 }
366 }
367
368 pub fn unique_count(&self) -> usize {
370 self.errors.len()
371 }
372
373 pub fn total_count(&self) -> usize {
375 self.errors.values().map(|e| e.count).sum()
376 }
377
378 pub fn errors(&self) -> Vec<&AggregatedError> {
380 self.order
381 .iter()
382 .filter_map(|k| self.errors.get(k))
383 .collect()
384 }
385
386 pub fn into_errors(self) -> Vec<AggregatedError> {
388 self.order
389 .into_iter()
390 .filter_map(|k| self.errors.get(&k).cloned())
391 .collect()
392 }
393
394 pub fn clear(&mut self) {
396 self.errors.clear();
397 self.order.clear();
398 }
399
400 pub fn errors_above_severity(&self, min: Severity) -> Vec<&AggregatedError> {
402 self.errors()
403 .into_iter()
404 .filter(|ae| ae.error.severity >= min)
405 .collect()
406 }
407}
408
409#[derive(Debug, Clone)]
415pub struct ErrorResponse {
416 pub errors: Vec<ErrorEntry>,
417}
418
419#[derive(Debug, Clone)]
421pub struct ErrorEntry {
422 pub message: String,
423 pub path: Option<Vec<String>>,
424 pub locations: Option<Vec<SourceLocation>>,
425 pub extensions: Option<HashMap<String, String>>,
426}
427
428pub struct ErrorResponseBuilder {
430 entries: Vec<ErrorEntry>,
431 default_timestamp: Option<String>,
432 default_trace_id: Option<String>,
433}
434
435impl Default for ErrorResponseBuilder {
436 fn default() -> Self {
437 Self::new()
438 }
439}
440
441impl ErrorResponseBuilder {
442 pub fn new() -> Self {
443 Self {
444 entries: Vec::new(),
445 default_timestamp: None,
446 default_trace_id: None,
447 }
448 }
449
450 pub fn with_timestamp(mut self, ts: impl Into<String>) -> Self {
452 self.default_timestamp = Some(ts.into());
453 self
454 }
455
456 pub fn with_trace_id(mut self, id: impl Into<String>) -> Self {
458 self.default_trace_id = Some(id.into());
459 self
460 }
461
462 pub fn add_error(&mut self, error: &GraphQLError) {
464 let path = if error.path.is_empty() {
465 None
466 } else {
467 Some(error.path.iter().map(|s| s.to_string()).collect())
468 };
469
470 let locations = if error.locations.is_empty() {
471 None
472 } else {
473 Some(error.locations.clone())
474 };
475
476 let mut ext_map = HashMap::new();
477 ext_map.insert("code".to_string(), error.classification.code().to_string());
478 ext_map.insert("severity".to_string(), error.severity.label().to_string());
479 ext_map.insert(
480 "classification".to_string(),
481 error.classification.label().to_string(),
482 );
483
484 if let Some(ref ts) = self.default_timestamp {
485 ext_map.insert("timestamp".to_string(), ts.clone());
486 }
487 if let Some(ref tid) = self.default_trace_id {
488 ext_map.insert("traceId".to_string(), tid.clone());
489 }
490
491 if let Some(ref exts) = error.extensions {
492 if !exts.code.is_empty() {
494 ext_map.insert("code".to_string(), exts.code.clone());
495 }
496 if let Some(ref ts) = exts.timestamp {
497 ext_map.insert("timestamp".to_string(), ts.clone());
498 }
499 if let Some(ref tid) = exts.trace_id {
500 ext_map.insert("traceId".to_string(), tid.clone());
501 }
502 for (k, v) in &exts.extra {
503 ext_map.insert(k.clone(), v.clone());
504 }
505 }
506
507 self.entries.push(ErrorEntry {
508 message: error.message.clone(),
509 path,
510 locations,
511 extensions: Some(ext_map),
512 });
513 }
514
515 pub fn add_aggregated(&mut self, aggregator: &ErrorAggregator) {
517 for ae in aggregator.errors() {
518 self.add_error(&ae.error);
519 if ae.count > 1 {
521 if let Some(entry) = self.entries.last_mut() {
522 if let Some(ref mut ext) = entry.extensions {
523 ext.insert("occurrences".to_string(), ae.count.to_string());
524 }
525 }
526 }
527 }
528 }
529
530 pub fn build(self) -> ErrorResponse {
532 ErrorResponse {
533 errors: self.entries,
534 }
535 }
536
537 pub fn entry_count(&self) -> usize {
539 self.entries.len()
540 }
541}
542
543pub fn format_response_json(response: &ErrorResponse) -> String {
545 let mut out = String::from("{\"errors\":[");
546 for (i, entry) in response.errors.iter().enumerate() {
547 if i > 0 {
548 out.push(',');
549 }
550 out.push_str("{\"message\":");
551 out.push_str(&json_escape(&entry.message));
552
553 if let Some(ref path) = entry.path {
554 out.push_str(",\"path\":[");
555 for (j, seg) in path.iter().enumerate() {
556 if j > 0 {
557 out.push(',');
558 }
559 if seg.parse::<usize>().is_ok() {
561 out.push_str(seg);
562 } else {
563 out.push_str(&json_escape(seg));
564 }
565 }
566 out.push(']');
567 }
568
569 if let Some(ref locs) = entry.locations {
570 out.push_str(",\"locations\":[");
571 for (j, loc) in locs.iter().enumerate() {
572 if j > 0 {
573 out.push(',');
574 }
575 out.push_str(&format!(
576 "{{\"line\":{},\"column\":{}}}",
577 loc.line, loc.column
578 ));
579 }
580 out.push(']');
581 }
582
583 if let Some(ref ext) = entry.extensions {
584 out.push_str(",\"extensions\":{");
585 let mut sorted_keys: Vec<&String> = ext.keys().collect();
586 sorted_keys.sort();
587 for (j, key) in sorted_keys.iter().enumerate() {
588 if j > 0 {
589 out.push(',');
590 }
591 if let Some(val) = ext.get(*key) {
592 out.push_str(&format!("{}:{}", json_escape(key), json_escape(val)));
593 }
594 }
595 out.push('}');
596 }
597
598 out.push('}');
599 }
600 out.push_str("]}");
601 out
602}
603
604fn json_escape(s: &str) -> String {
606 let mut out = String::with_capacity(s.len() + 2);
607 out.push('"');
608 for ch in s.chars() {
609 match ch {
610 '"' => out.push_str("\\\""),
611 '\\' => out.push_str("\\\\"),
612 '\n' => out.push_str("\\n"),
613 '\r' => out.push_str("\\r"),
614 '\t' => out.push_str("\\t"),
615 c => out.push(c),
616 }
617 }
618 out.push('"');
619 out
620}
621
622#[cfg(test)]
627mod tests {
628 use super::*;
629
630 #[test]
633 fn test_error_class_codes() {
634 assert_eq!(ErrorClass::Validation.code(), "VALIDATION_ERROR");
635 assert_eq!(ErrorClass::Execution.code(), "EXECUTION_ERROR");
636 assert_eq!(ErrorClass::Internal.code(), "INTERNAL_ERROR");
637 assert_eq!(ErrorClass::Authorization.code(), "AUTHORIZATION_ERROR");
638 assert_eq!(ErrorClass::RateLimit.code(), "RATE_LIMIT_ERROR");
639 assert_eq!(ErrorClass::InputCoercion.code(), "INPUT_COERCION_ERROR");
640 }
641
642 #[test]
643 fn test_error_class_labels() {
644 assert_eq!(ErrorClass::Validation.label(), "Validation Error");
645 assert_eq!(ErrorClass::Internal.label(), "Internal Error");
646 }
647
648 #[test]
651 fn test_severity_ordering() {
652 assert!(Severity::Info < Severity::Warning);
653 assert!(Severity::Warning < Severity::Error);
654 assert!(Severity::Error < Severity::Critical);
655 }
656
657 #[test]
658 fn test_severity_labels() {
659 assert_eq!(Severity::Info.label(), "info");
660 assert_eq!(Severity::Warning.label(), "warning");
661 assert_eq!(Severity::Error.label(), "error");
662 assert_eq!(Severity::Critical.label(), "critical");
663 }
664
665 #[test]
666 fn test_severity_from_class() {
667 assert_eq!(
668 Severity::from_class(ErrorClass::Validation),
669 Severity::Error
670 );
671 assert_eq!(
672 Severity::from_class(ErrorClass::Internal),
673 Severity::Critical
674 );
675 assert_eq!(
676 Severity::from_class(ErrorClass::RateLimit),
677 Severity::Warning
678 );
679 }
680
681 #[test]
684 fn test_path_segment_display() {
685 assert_eq!(PathSegment::Field("user".into()).to_string(), "user");
686 assert_eq!(PathSegment::Index(3).to_string(), "3");
687 }
688
689 #[test]
690 fn test_format_path() {
691 let path = vec![
692 PathSegment::field("user"),
693 PathSegment::field("friends"),
694 PathSegment::index(0),
695 PathSegment::field("name"),
696 ];
697 assert_eq!(format_path(&path), "user.friends.0.name");
698 }
699
700 #[test]
701 fn test_format_path_empty() {
702 assert_eq!(format_path(&[]), "");
703 }
704
705 #[test]
708 fn test_extensions_builder() {
709 let ext = ErrorExtensions::new("CUSTOM_CODE")
710 .with_timestamp("2026-03-03T00:00:00Z")
711 .with_trace_id("trace-abc-123")
712 .with_extra("service", "gql-server");
713
714 assert_eq!(ext.code, "CUSTOM_CODE");
715 assert_eq!(ext.timestamp, Some("2026-03-03T00:00:00Z".to_string()));
716 assert_eq!(ext.trace_id, Some("trace-abc-123".to_string()));
717 assert_eq!(ext.extra.get("service"), Some(&"gql-server".to_string()));
718 }
719
720 #[test]
723 fn test_error_basic() {
724 let err = GraphQLError::new("field not found", ErrorClass::Validation);
725 assert_eq!(err.message, "field not found");
726 assert_eq!(err.classification, ErrorClass::Validation);
727 assert_eq!(err.severity, Severity::Error);
728 assert!(err.path.is_empty());
729 }
730
731 #[test]
732 fn test_error_with_path() {
733 let err = GraphQLError::new("null value", ErrorClass::Execution)
734 .with_path(vec![PathSegment::field("user"), PathSegment::field("name")]);
735 assert_eq!(err.path_string(), "user.name");
736 }
737
738 #[test]
739 fn test_error_with_locations() {
740 let err = GraphQLError::new("syntax error", ErrorClass::Validation)
741 .with_locations(vec![SourceLocation::new(3, 10)]);
742 assert_eq!(err.locations.len(), 1);
743 assert_eq!(err.locations[0].line, 3);
744 assert_eq!(err.locations[0].column, 10);
745 }
746
747 #[test]
748 fn test_error_with_severity_override() {
749 let err = GraphQLError::new("deprecated field", ErrorClass::Execution)
750 .with_severity(Severity::Warning);
751 assert_eq!(err.severity, Severity::Warning);
752 }
753
754 #[test]
755 fn test_error_with_code() {
756 let err = GraphQLError::new("test", ErrorClass::Internal).with_code("CUSTOM");
757 assert!(err.extensions.is_some());
758 let ext = err.extensions.as_ref().expect("should have extensions");
759 assert_eq!(ext.code, "CUSTOM");
760 }
761
762 #[test]
763 fn test_error_dedup_key() {
764 let err1 = GraphQLError::new("bad field", ErrorClass::Validation)
765 .with_path(vec![PathSegment::field("user")]);
766 let err2 = GraphQLError::new("bad field", ErrorClass::Validation)
767 .with_path(vec![PathSegment::field("user")]);
768 assert_eq!(err1.dedup_key(), err2.dedup_key());
769 }
770
771 #[test]
772 fn test_error_dedup_key_different() {
773 let err1 = GraphQLError::new("bad field", ErrorClass::Validation);
774 let err2 = GraphQLError::new("other error", ErrorClass::Validation);
775 assert_ne!(err1.dedup_key(), err2.dedup_key());
776 }
777
778 #[test]
781 fn test_message_registry_register_and_get() {
782 let mut reg = ErrorMessageRegistry::new();
783 reg.register("AUTH_FAIL", "Please login to continue.");
784 assert_eq!(reg.get("AUTH_FAIL"), Some("Please login to continue."));
785 assert_eq!(reg.get("OTHER"), None);
786 }
787
788 #[test]
789 fn test_message_registry_apply() {
790 let mut reg = ErrorMessageRegistry::new();
791 reg.register("CUSTOM", "A friendly error message");
792
793 let mut err = GraphQLError::new("raw error", ErrorClass::Execution)
794 .with_extensions(ErrorExtensions::new("CUSTOM"));
795 reg.apply(&mut err);
796 assert_eq!(err.message, "A friendly error message");
797 }
798
799 #[test]
800 fn test_message_registry_no_match() {
801 let mut reg = ErrorMessageRegistry::new();
802 reg.register("OTHER", "something");
803
804 let mut err = GraphQLError::new("original", ErrorClass::Execution)
805 .with_extensions(ErrorExtensions::new("NOMATCH"));
806 reg.apply(&mut err);
807 assert_eq!(err.message, "original");
808 }
809
810 #[test]
811 fn test_message_registry_counts() {
812 let mut reg = ErrorMessageRegistry::new();
813 assert!(reg.is_empty());
814 reg.register("A", "msg A");
815 reg.register("B", "msg B");
816 assert_eq!(reg.len(), 2);
817 assert!(!reg.is_empty());
818 }
819
820 #[test]
821 fn test_message_registry_default() {
822 let reg = ErrorMessageRegistry::default();
823 assert!(reg.is_empty());
824 }
825
826 #[test]
829 fn test_aggregator_dedup() {
830 let mut agg = ErrorAggregator::new();
831 agg.add(GraphQLError::new("same error", ErrorClass::Execution));
832 agg.add(GraphQLError::new("same error", ErrorClass::Execution));
833 agg.add(GraphQLError::new("different", ErrorClass::Execution));
834
835 assert_eq!(agg.unique_count(), 2);
836 assert_eq!(agg.total_count(), 3);
837 }
838
839 #[test]
840 fn test_aggregator_order_preserved() {
841 let mut agg = ErrorAggregator::new();
842 agg.add(GraphQLError::new("first", ErrorClass::Validation));
843 agg.add(GraphQLError::new("second", ErrorClass::Execution));
844 agg.add(GraphQLError::new("third", ErrorClass::Internal));
845
846 let errors = agg.errors();
847 assert_eq!(errors[0].error.message, "first");
848 assert_eq!(errors[1].error.message, "second");
849 assert_eq!(errors[2].error.message, "third");
850 }
851
852 #[test]
853 fn test_aggregator_clear() {
854 let mut agg = ErrorAggregator::new();
855 agg.add(GraphQLError::new("e", ErrorClass::Execution));
856 agg.clear();
857 assert_eq!(agg.unique_count(), 0);
858 assert_eq!(agg.total_count(), 0);
859 }
860
861 #[test]
862 fn test_aggregator_into_errors() {
863 let mut agg = ErrorAggregator::new();
864 agg.add(GraphQLError::new("e1", ErrorClass::Execution));
865 agg.add(GraphQLError::new("e2", ErrorClass::Execution));
866 let errors = agg.into_errors();
867 assert_eq!(errors.len(), 2);
868 }
869
870 #[test]
871 fn test_aggregator_severity_filter() {
872 let mut agg = ErrorAggregator::new();
873 agg.add(GraphQLError::new("info", ErrorClass::Execution).with_severity(Severity::Info));
874 agg.add(GraphQLError::new("warn", ErrorClass::RateLimit).with_severity(Severity::Warning));
875 agg.add(GraphQLError::new("err", ErrorClass::Execution));
876 agg.add(GraphQLError::new("crit", ErrorClass::Internal));
877
878 let critical = agg.errors_above_severity(Severity::Critical);
879 assert_eq!(critical.len(), 1);
880 assert_eq!(critical[0].error.message, "crit");
881
882 let warnings = agg.errors_above_severity(Severity::Warning);
883 assert_eq!(warnings.len(), 3);
884 }
885
886 #[test]
887 fn test_aggregator_default() {
888 let agg = ErrorAggregator::default();
889 assert_eq!(agg.unique_count(), 0);
890 }
891
892 #[test]
895 fn test_response_builder_single_error() {
896 let mut builder = ErrorResponseBuilder::new();
897 let err = GraphQLError::new("not found", ErrorClass::Execution)
898 .with_path(vec![PathSegment::field("user")]);
899 builder.add_error(&err);
900
901 let response = builder.build();
902 assert_eq!(response.errors.len(), 1);
903 assert_eq!(response.errors[0].message, "not found");
904 assert!(response.errors[0].path.is_some());
905 }
906
907 #[test]
908 fn test_response_builder_with_timestamp() {
909 let mut builder = ErrorResponseBuilder::new().with_timestamp("2026-03-03T00:00:00Z");
910 builder.add_error(&GraphQLError::new("err", ErrorClass::Internal));
911
912 let response = builder.build();
913 let ext = response.errors[0].extensions.as_ref().expect("extensions");
914 assert_eq!(
915 ext.get("timestamp"),
916 Some(&"2026-03-03T00:00:00Z".to_string())
917 );
918 }
919
920 #[test]
921 fn test_response_builder_with_trace_id() {
922 let mut builder = ErrorResponseBuilder::new().with_trace_id("trace-42");
923 builder.add_error(&GraphQLError::new("err", ErrorClass::Execution));
924
925 let response = builder.build();
926 let ext = response.errors[0].extensions.as_ref().expect("extensions");
927 assert_eq!(ext.get("traceId"), Some(&"trace-42".to_string()));
928 }
929
930 #[test]
931 fn test_response_builder_aggregated() {
932 let mut agg = ErrorAggregator::new();
933 agg.add(GraphQLError::new("dup", ErrorClass::Execution));
934 agg.add(GraphQLError::new("dup", ErrorClass::Execution));
935
936 let mut builder = ErrorResponseBuilder::new();
937 builder.add_aggregated(&agg);
938
939 let response = builder.build();
940 assert_eq!(response.errors.len(), 1);
941 let ext = response.errors[0].extensions.as_ref().expect("extensions");
942 assert_eq!(ext.get("occurrences"), Some(&"2".to_string()));
943 }
944
945 #[test]
946 fn test_response_builder_entry_count() {
947 let mut builder = ErrorResponseBuilder::new();
948 assert_eq!(builder.entry_count(), 0);
949 builder.add_error(&GraphQLError::new("a", ErrorClass::Execution));
950 builder.add_error(&GraphQLError::new("b", ErrorClass::Validation));
951 assert_eq!(builder.entry_count(), 2);
952 }
953
954 #[test]
955 fn test_response_builder_default() {
956 let builder = ErrorResponseBuilder::default();
957 assert_eq!(builder.entry_count(), 0);
958 }
959
960 #[test]
963 fn test_format_response_json_basic() {
964 let mut builder = ErrorResponseBuilder::new();
965 builder.add_error(&GraphQLError::new("oops", ErrorClass::Execution));
966 let response = builder.build();
967
968 let json = format_response_json(&response);
969 assert!(json.contains("\"errors\""));
970 assert!(json.contains("oops"));
971 assert!(json.contains("EXECUTION_ERROR"));
972 }
973
974 #[test]
975 fn test_format_response_json_with_path() {
976 let mut builder = ErrorResponseBuilder::new();
977 builder.add_error(
978 &GraphQLError::new("null", ErrorClass::Execution)
979 .with_path(vec![PathSegment::field("user"), PathSegment::index(0)]),
980 );
981 let response = builder.build();
982
983 let json = format_response_json(&response);
984 assert!(json.contains("\"path\""));
985 assert!(json.contains("\"user\""));
986 assert!(json.contains("0"));
987 }
988
989 #[test]
990 fn test_format_response_json_with_locations() {
991 let mut builder = ErrorResponseBuilder::new();
992 builder.add_error(
993 &GraphQLError::new("parse error", ErrorClass::Validation)
994 .with_locations(vec![SourceLocation::new(5, 12)]),
995 );
996 let response = builder.build();
997
998 let json = format_response_json(&response);
999 assert!(json.contains("\"locations\""));
1000 assert!(json.contains("\"line\":5"));
1001 assert!(json.contains("\"column\":12"));
1002 }
1003
1004 #[test]
1005 fn test_format_response_json_empty() {
1006 let builder = ErrorResponseBuilder::new();
1007 let response = builder.build();
1008 let json = format_response_json(&response);
1009 assert_eq!(json, "{\"errors\":[]}");
1010 }
1011
1012 #[test]
1013 fn test_format_response_json_multiple_errors() {
1014 let mut builder = ErrorResponseBuilder::new();
1015 builder.add_error(&GraphQLError::new("e1", ErrorClass::Execution));
1016 builder.add_error(&GraphQLError::new("e2", ErrorClass::Validation));
1017 let response = builder.build();
1018
1019 let json = format_response_json(&response);
1020 assert!(json.contains("e1"));
1021 assert!(json.contains("e2"));
1022 }
1023
1024 #[test]
1027 fn test_json_escape_simple() {
1028 assert_eq!(json_escape("hello"), "\"hello\"");
1029 }
1030
1031 #[test]
1032 fn test_json_escape_special_chars() {
1033 assert_eq!(json_escape("a\"b"), "\"a\\\"b\"");
1034 assert_eq!(json_escape("a\\b"), "\"a\\\\b\"");
1035 assert_eq!(json_escape("a\nb"), "\"a\\nb\"");
1036 assert_eq!(json_escape("a\tb"), "\"a\\tb\"");
1037 assert_eq!(json_escape("a\rb"), "\"a\\rb\"");
1038 }
1039
1040 #[test]
1043 fn test_source_location_new() {
1044 let loc = SourceLocation::new(10, 20);
1045 assert_eq!(loc.line, 10);
1046 assert_eq!(loc.column, 20);
1047 }
1048
1049 #[test]
1052 fn test_error_extensions_override_builder_defaults() {
1053 let mut builder = ErrorResponseBuilder::new().with_timestamp("builder-ts");
1054
1055 let err = GraphQLError::new("test", ErrorClass::Execution)
1056 .with_extensions(ErrorExtensions::new("CODE").with_timestamp("error-ts"));
1057 builder.add_error(&err);
1058
1059 let response = builder.build();
1060 let ext = response.errors[0].extensions.as_ref().expect("extensions");
1061 assert_eq!(ext.get("timestamp"), Some(&"error-ts".to_string()));
1063 }
1064
1065 #[test]
1066 fn test_error_path_string_empty() {
1067 let err = GraphQLError::new("e", ErrorClass::Execution);
1068 assert_eq!(err.path_string(), "");
1069 }
1070
1071 #[test]
1074 fn test_full_pipeline_aggregation_to_response() {
1075 let mut agg = ErrorAggregator::new();
1076
1077 agg.add(
1079 GraphQLError::new("field 'xyz' not found", ErrorClass::Validation)
1080 .with_path(vec![PathSegment::field("query"), PathSegment::field("xyz")])
1081 .with_locations(vec![SourceLocation::new(1, 5)])
1082 .with_extensions(ErrorExtensions::new("UNKNOWN_FIELD").with_trace_id("t-001")),
1083 );
1084
1085 agg.add(
1087 GraphQLError::new("resolver failed", ErrorClass::Execution)
1088 .with_path(vec![PathSegment::field("user"), PathSegment::field("name")]),
1089 );
1090 agg.add(
1091 GraphQLError::new("resolver failed", ErrorClass::Execution)
1092 .with_path(vec![PathSegment::field("user"), PathSegment::field("name")]),
1093 );
1094
1095 assert_eq!(agg.unique_count(), 2);
1096 assert_eq!(agg.total_count(), 3);
1097
1098 let mut builder = ErrorResponseBuilder::new()
1099 .with_timestamp("2026-03-03T12:00:00Z")
1100 .with_trace_id("global-trace");
1101
1102 builder.add_aggregated(&agg);
1103 let response = builder.build();
1104
1105 assert_eq!(response.errors.len(), 2);
1106
1107 let ext0 = response.errors[0].extensions.as_ref().expect("extensions");
1109 assert_eq!(ext0.get("traceId"), Some(&"t-001".to_string()));
1110
1111 let ext1 = response.errors[1].extensions.as_ref().expect("extensions");
1113 assert_eq!(ext1.get("occurrences"), Some(&"2".to_string()));
1114
1115 let json = format_response_json(&response);
1116 assert!(json.contains("UNKNOWN_FIELD"));
1117 assert!(json.contains("resolver failed"));
1118 }
1119}