1use crate::ast::{
7 Definition, Document, Field, OperationDefinition, Selection, SelectionSet, Value,
8 VariableDefinition,
9};
10use crate::types::{GraphQLType, Schema};
11use anyhow::{anyhow, Result};
12use std::collections::{HashMap, HashSet};
13use std::time::Duration;
14
15#[derive(Debug, Clone)]
17pub struct ValidationConfig {
18 pub max_depth: usize,
20 pub max_complexity: usize,
22 pub max_aliases: usize,
24 pub max_root_fields: usize,
26 pub query_timeout: Option<Duration>,
28 pub disable_introspection: bool,
30 pub max_fragments: usize,
32 pub allowed_operations: Option<HashSet<String>>,
34 pub forbidden_fields: HashSet<String>,
36 pub enable_cost_analysis: bool,
38}
39
40impl Default for ValidationConfig {
41 fn default() -> Self {
42 Self {
43 max_depth: 10,
44 max_complexity: 1000,
45 max_aliases: 50,
46 max_root_fields: 20,
47 query_timeout: Some(Duration::from_secs(30)),
48 disable_introspection: false,
49 max_fragments: 50,
50 allowed_operations: None,
51 forbidden_fields: HashSet::new(),
52 enable_cost_analysis: true,
53 }
54 }
55}
56
57#[derive(Debug, Clone)]
59pub struct ValidationResult {
60 pub is_valid: bool,
61 pub errors: Vec<ValidationError>,
62 pub warnings: Vec<ValidationWarning>,
63 pub complexity_score: usize,
64 pub depth: usize,
65}
66
67impl Default for ValidationResult {
68 fn default() -> Self {
69 Self::new()
70 }
71}
72
73impl ValidationResult {
74 pub fn new() -> Self {
75 Self {
76 is_valid: true,
77 errors: Vec::new(),
78 warnings: Vec::new(),
79 complexity_score: 0,
80 depth: 0,
81 }
82 }
83
84 pub fn with_error(mut self, error: ValidationError) -> Self {
85 self.is_valid = false;
86 self.errors.push(error);
87 self
88 }
89
90 pub fn with_warning(mut self, warning: ValidationWarning) -> Self {
91 self.warnings.push(warning);
92 self
93 }
94
95 pub fn with_complexity(mut self, complexity: usize) -> Self {
96 self.complexity_score = complexity;
97 self
98 }
99
100 pub fn with_depth(mut self, depth: usize) -> Self {
101 self.depth = depth;
102 self
103 }
104}
105
106#[derive(Debug, Clone)]
108pub struct ValidationError {
109 pub message: String,
110 pub path: Vec<String>,
111 pub rule: ValidationRule,
112}
113
114impl ValidationError {
115 pub fn new(message: String, rule: ValidationRule) -> Self {
116 Self {
117 message,
118 path: Vec::new(),
119 rule,
120 }
121 }
122
123 pub fn with_path(mut self, path: Vec<String>) -> Self {
124 self.path = path;
125 self
126 }
127}
128
129#[derive(Debug, Clone)]
131pub struct ValidationWarning {
132 pub message: String,
133 pub suggestion: Option<String>,
134}
135
136impl ValidationWarning {
137 pub fn new(message: String) -> Self {
138 Self {
139 message,
140 suggestion: None,
141 }
142 }
143
144 pub fn with_suggestion(mut self, suggestion: String) -> Self {
145 self.suggestion = Some(suggestion);
146 self
147 }
148}
149
150#[derive(Debug, Clone, PartialEq)]
152pub enum ValidationRule {
153 MaxDepth,
154 MaxComplexity,
155 MaxAliases,
156 MaxRootFields,
157 MaxFragments,
158 FieldValidation,
159 TypeValidation,
160 VariableValidation,
161 FragmentValidation,
162 IntrospectionDisabled,
163 OperationNotAllowed,
164 ForbiddenField,
165 CostAnalysis,
166 Spec(SpecRule),
168}
169
170#[derive(Debug, Clone, PartialEq)]
172pub enum SpecRule {
173 ExecutableDefinitions,
174 UniqueOperationNames,
175 LoneAnonymousOperation,
176 SingleFieldSubscriptions,
177 KnownTypeNames,
178 FragmentsOnCompositeTypes,
179 VariablesAreInputTypes,
180 LeafFieldSelections,
181 FieldsInSetCanMerge,
182 UniqueFragmentNames,
183 KnownFragmentNames,
184 NoUnusedFragments,
185 PossibleFragmentSpreads,
186 NoFragmentCycles,
187 UniqueVariableNames,
188 NoUndefinedVariables,
189 NoUnusedVariables,
190 KnownDirectives,
191 UniqueDirectivesPerLocation,
192 KnownArgumentNames,
193 UniqueArgumentNames,
194 ValuesOfCorrectType,
195 ProvidedRequiredArguments,
196 VariablesInAllowedPosition,
197 OverlappingFieldsCanBeMerged,
198}
199
200pub struct QueryValidator {
202 config: ValidationConfig,
203 schema: Schema,
204}
205
206impl QueryValidator {
207 pub fn new(config: ValidationConfig, schema: Schema) -> Self {
208 Self { config, schema }
209 }
210
211 pub fn validate(&self, document: &Document) -> Result<ValidationResult> {
213 let mut result = ValidationResult::new();
214 let mut validation_context = ValidationContext::new(&self.schema);
215
216 for definition in &document.definitions {
218 if let Definition::Fragment(fragment) = definition {
219 validation_context.add_fragment(fragment.name.clone(), fragment.clone());
220 }
221 }
222
223 for definition in &document.definitions {
225 if let Definition::Operation(operation) = definition {
226 result = self.validate_operation(operation, &validation_context, result)?;
227 }
228 }
229
230 result = self.validate_fragments(&validation_context, result)?;
232 result = self.validate_global_limits(document, result)?;
233
234 Ok(result)
235 }
236
237 fn validate_operation(
238 &self,
239 operation: &OperationDefinition,
240 context: &ValidationContext,
241 mut result: ValidationResult,
242 ) -> Result<ValidationResult> {
243 if let Some(ref allowed_ops) = self.config.allowed_operations {
245 if let Some(ref op_name) = operation.name {
246 if !allowed_ops.contains(op_name) {
247 return Ok(result.with_error(ValidationError::new(
248 format!("Operation '{op_name}' is not allowed"),
249 ValidationRule::OperationNotAllowed,
250 )));
251 }
252 }
253 }
254
255 result = self.validate_variables(&operation.variable_definitions, context, result)?;
257
258 let root_type_name = match operation.operation_type {
260 crate::ast::OperationType::Query => self
261 .schema
262 .query_type
263 .as_ref()
264 .ok_or_else(|| anyhow!("Schema has no query type"))?,
265 crate::ast::OperationType::Mutation => self
266 .schema
267 .mutation_type
268 .as_ref()
269 .ok_or_else(|| anyhow!("Schema has no mutation type"))?,
270 crate::ast::OperationType::Subscription => self
271 .schema
272 .subscription_type
273 .as_ref()
274 .ok_or_else(|| anyhow!("Schema has no subscription type"))?,
275 };
276
277 let (depth, complexity) = self.validate_selection_set(
279 &operation.selection_set,
280 root_type_name,
281 context,
282 0,
283 Vec::new(),
284 )?;
285
286 result.depth = depth.max(result.depth);
287 result.complexity_score += complexity;
288
289 if result.depth > self.config.max_depth {
291 let current_depth = result.depth;
292 result = result.with_error(ValidationError::new(
293 format!(
294 "Query depth {} exceeds maximum allowed depth {}",
295 current_depth, self.config.max_depth
296 ),
297 ValidationRule::MaxDepth,
298 ));
299 }
300
301 if result.complexity_score > self.config.max_complexity {
303 let current_complexity = result.complexity_score;
304 result = result.with_error(ValidationError::new(
305 format!(
306 "Query complexity {} exceeds maximum allowed complexity {}",
307 current_complexity, self.config.max_complexity
308 ),
309 ValidationRule::MaxComplexity,
310 ));
311 }
312
313 Ok(result)
314 }
315
316 fn validate_selection_set(
317 &self,
318 selection_set: &SelectionSet,
319 parent_type_name: &str,
320 context: &ValidationContext,
321 current_depth: usize,
322 path: Vec<String>,
323 ) -> Result<(usize, usize)> {
324 let mut max_depth = current_depth;
325 let mut total_complexity = 0;
326 let mut alias_count = 0;
327
328 let parent_type = self
329 .schema
330 .get_type(parent_type_name)
331 .ok_or_else(|| anyhow!("Type '{}' not found in schema", parent_type_name))?;
332
333 for selection in &selection_set.selections {
334 match selection {
335 Selection::Field(field) => {
336 if field.alias.is_some() {
338 alias_count += 1;
339 }
340
341 if self.config.forbidden_fields.contains(&field.name) {
343 return Err(anyhow!("Field '{}' is forbidden", field.name));
344 }
345
346 if self.config.disable_introspection && field.name.starts_with("__") {
348 return Err(anyhow!("Introspection is disabled"));
349 }
350
351 let field_type = self.get_field_type(parent_type, &field.name)?;
353
354 let mut field_path = path.clone();
355 field_path.push(field.alias.as_ref().unwrap_or(&field.name).clone());
356
357 let field_complexity = self.calculate_field_complexity(field);
359 total_complexity += field_complexity;
360
361 if let Some(ref nested_selection_set) = field.selection_set {
363 let inner_type_name = self.get_inner_type_name(field_type);
364 let (nested_depth, nested_complexity) = self.validate_selection_set(
365 nested_selection_set,
366 &inner_type_name,
367 context,
368 current_depth + 1,
369 field_path,
370 )?;
371 max_depth = max_depth.max(nested_depth);
372 total_complexity += nested_complexity;
373 }
374 }
375 Selection::InlineFragment(inline_fragment) => {
376 let fragment_type =
377 if let Some(ref type_condition) = inline_fragment.type_condition {
378 type_condition
379 } else {
380 parent_type_name
381 };
382
383 let (nested_depth, nested_complexity) = self.validate_selection_set(
384 &inline_fragment.selection_set,
385 fragment_type,
386 context,
387 current_depth,
388 path.clone(),
389 )?;
390 max_depth = max_depth.max(nested_depth);
391 total_complexity += nested_complexity;
392 }
393 Selection::FragmentSpread(fragment_spread) => {
394 if let Some(fragment_def) = context.get_fragment(&fragment_spread.fragment_name)
395 {
396 let (nested_depth, nested_complexity) = self.validate_selection_set(
397 &fragment_def.selection_set,
398 &fragment_def.type_condition,
399 context,
400 current_depth,
401 path.clone(),
402 )?;
403 max_depth = max_depth.max(nested_depth);
404 total_complexity += nested_complexity;
405 } else {
406 return Err(anyhow!(
407 "Fragment '{}' not found",
408 fragment_spread.fragment_name
409 ));
410 }
411 }
412 }
413 }
414
415 if alias_count > self.config.max_aliases {
417 return Err(anyhow!(
418 "Too many aliases: {} exceeds limit {}",
419 alias_count,
420 self.config.max_aliases
421 ));
422 }
423
424 Ok((max_depth, total_complexity))
425 }
426
427 fn validate_variables(
428 &self,
429 variable_definitions: &[VariableDefinition],
430 _context: &ValidationContext,
431 mut result: ValidationResult,
432 ) -> Result<ValidationResult> {
433 for var_def in variable_definitions {
434 if !self.type_exists(&var_def.type_) {
436 result = result.with_error(ValidationError::new(
437 format!(
438 "Variable type '{}' not found in schema",
439 var_def.type_.name()
440 ),
441 ValidationRule::VariableValidation,
442 ));
443 }
444
445 if let Some(ref default_value) = var_def.default_value {
447 if !self.is_value_compatible_with_type(default_value, &var_def.type_) {
448 result = result.with_error(ValidationError::new(
449 format!(
450 "Default value for variable '{}' is not compatible with type '{}'",
451 var_def.variable.name,
452 var_def.type_.name()
453 ),
454 ValidationRule::VariableValidation,
455 ));
456 }
457 }
458 }
459
460 Ok(result)
461 }
462
463 fn validate_fragments(
464 &self,
465 context: &ValidationContext,
466 mut result: ValidationResult,
467 ) -> Result<ValidationResult> {
468 if context.fragments.len() > self.config.max_fragments {
469 result = result.with_error(ValidationError::new(
470 format!(
471 "Too many fragments: {} exceeds limit {}",
472 context.fragments.len(),
473 self.config.max_fragments
474 ),
475 ValidationRule::MaxFragments,
476 ));
477 }
478
479 for (fragment_name, fragment) in &context.fragments {
481 if !self.schema.types.contains_key(&fragment.type_condition) {
482 result = result.with_error(ValidationError::new(
483 format!(
484 "Fragment '{}' has unknown type condition '{}'",
485 fragment_name, fragment.type_condition
486 ),
487 ValidationRule::FragmentValidation,
488 ));
489 }
490 }
491
492 Ok(result)
493 }
494
495 fn validate_global_limits(
496 &self,
497 document: &Document,
498 mut result: ValidationResult,
499 ) -> Result<ValidationResult> {
500 let mut root_field_count = 0;
501
502 for definition in &document.definitions {
503 if let Definition::Operation(operation) = definition {
504 root_field_count += operation.selection_set.selections.len();
505 }
506 }
507
508 if root_field_count > self.config.max_root_fields {
509 result = result.with_error(ValidationError::new(
510 format!(
511 "Too many root fields: {} exceeds limit {}",
512 root_field_count, self.config.max_root_fields
513 ),
514 ValidationRule::MaxRootFields,
515 ));
516 }
517
518 Ok(result)
519 }
520
521 fn get_field_type<'a>(
522 &self,
523 parent_type: &'a GraphQLType,
524 field_name: &str,
525 ) -> Result<&'a GraphQLType> {
526 match parent_type {
527 GraphQLType::Object(obj) => obj
528 .fields
529 .get(field_name)
530 .map(|field| &field.field_type)
531 .ok_or_else(|| {
532 anyhow!(
533 "Field '{}' not found on object type '{}'",
534 field_name,
535 obj.name
536 )
537 }),
538 GraphQLType::Interface(iface) => iface
539 .fields
540 .get(field_name)
541 .map(|field| &field.field_type)
542 .ok_or_else(|| {
543 anyhow!(
544 "Field '{}' not found on interface type '{}'",
545 field_name,
546 iface.name
547 )
548 }),
549 _ => Err(anyhow!(
550 "Cannot select field '{}' on non-composite type",
551 field_name
552 )),
553 }
554 }
555
556 #[allow(clippy::only_used_in_recursion)]
557 fn get_inner_type_name(&self, graphql_type: &GraphQLType) -> String {
558 match graphql_type {
559 GraphQLType::NonNull(inner) => self.get_inner_type_name(inner),
560 GraphQLType::List(inner) => self.get_inner_type_name(inner),
561 _ => graphql_type.name().to_string(),
562 }
563 }
564
565 fn calculate_field_complexity(&self, field: &Field) -> usize {
566 if !self.config.enable_cost_analysis {
567 return 1;
568 }
569
570 let mut complexity = 1;
571
572 complexity += field.arguments.len();
574
575 if let Some(ref selection_set) = field.selection_set {
577 complexity += selection_set.selections.len();
578 }
579
580 match field.name.as_str() {
582 "sparql" => complexity *= 10, name if name.contains("search") => complexity *= 3,
584 name if name.contains("aggregate") => complexity *= 5,
585 _ => {}
586 }
587
588 complexity
589 }
590
591 fn type_exists(&self, ast_type: &crate::ast::Type) -> bool {
592 match ast_type {
593 crate::ast::Type::NamedType(name) => {
594 self.schema.types.contains_key(name)
595 || matches!(name.as_str(), "String" | "Int" | "Float" | "Boolean" | "ID")
596 }
597 crate::ast::Type::ListType(inner) => self.type_exists(inner),
598 crate::ast::Type::NonNullType(inner) => self.type_exists(inner),
599 }
600 }
601
602 #[allow(clippy::only_used_in_recursion)]
603 fn is_value_compatible_with_type(&self, value: &Value, ast_type: &crate::ast::Type) -> bool {
604 match (value, ast_type) {
605 (Value::NullValue, crate::ast::Type::NonNullType(_)) => false,
606 (Value::NullValue, _) => true,
607 (Value::StringValue(_), crate::ast::Type::NamedType(name)) => {
608 matches!(name.as_str(), "String" | "ID")
609 }
610 (Value::IntValue(_), crate::ast::Type::NamedType(name)) => {
611 matches!(name.as_str(), "Int" | "ID")
612 }
613 (Value::FloatValue(_), crate::ast::Type::NamedType(name)) => {
614 matches!(name.as_str(), "Float")
615 }
616 (Value::BooleanValue(_), crate::ast::Type::NamedType(name)) => {
617 matches!(name.as_str(), "Boolean")
618 }
619 (Value::ListValue(list), crate::ast::Type::ListType(inner_type)) => list
620 .iter()
621 .all(|item| self.is_value_compatible_with_type(item, inner_type)),
622 (_, crate::ast::Type::NonNullType(inner)) => {
623 self.is_value_compatible_with_type(value, inner)
624 }
625 _ => false,
626 }
627 }
628}
629
630struct ValidationContext {
632 #[allow(dead_code)]
633 schema: Schema,
634 fragments: HashMap<String, crate::ast::FragmentDefinition>,
635}
636
637impl ValidationContext {
638 fn new(schema: &Schema) -> Self {
639 Self {
640 schema: schema.clone(),
641 fragments: HashMap::new(),
642 }
643 }
644
645 fn add_fragment(&mut self, name: String, fragment: crate::ast::FragmentDefinition) {
646 self.fragments.insert(name, fragment);
647 }
648
649 fn get_fragment(&self, name: &str) -> Option<&crate::ast::FragmentDefinition> {
650 self.fragments.get(name)
651 }
652}
653
654#[derive(Debug, Clone)]
656pub struct RateLimitConfig {
657 pub max_queries_per_minute: usize,
659 pub max_complexity_per_minute: usize,
661 pub window_duration: Duration,
663}
664
665impl Default for RateLimitConfig {
666 fn default() -> Self {
667 Self {
668 max_queries_per_minute: 60,
669 max_complexity_per_minute: 10000,
670 window_duration: Duration::from_secs(60),
671 }
672 }
673}
674
675#[cfg(test)]
676mod tests {
677 use super::*;
678 use crate::types::{BuiltinScalars, FieldType, ObjectType};
679
680 fn create_test_schema() -> Schema {
681 let mut schema = Schema::new();
682
683 let query_type = ObjectType::new("Query".to_string())
684 .with_field(
685 "hello".to_string(),
686 FieldType::new(
687 "hello".to_string(),
688 GraphQLType::Scalar(BuiltinScalars::string()),
689 ),
690 )
691 .with_field(
692 "__schema".to_string(),
693 FieldType::new(
694 "__schema".to_string(),
695 GraphQLType::Scalar(BuiltinScalars::string()),
696 ),
697 );
698
699 schema.add_type(GraphQLType::Object(query_type));
700 schema.set_query_type("Query".to_string());
701
702 schema
703 }
704
705 #[test]
706 fn test_validation_config_default() {
707 let config = ValidationConfig::default();
708 assert_eq!(config.max_depth, 10);
709 assert_eq!(config.max_complexity, 1000);
710 assert!(!config.disable_introspection);
711 }
712
713 #[test]
714 fn test_validation_result_creation() {
715 let result = ValidationResult::new()
716 .with_error(ValidationError::new(
717 "Test error".to_string(),
718 ValidationRule::MaxDepth,
719 ))
720 .with_warning(ValidationWarning::new("Test warning".to_string()))
721 .with_complexity(100)
722 .with_depth(5);
723
724 assert!(!result.is_valid);
725 assert_eq!(result.errors.len(), 1);
726 assert_eq!(result.warnings.len(), 1);
727 assert_eq!(result.complexity_score, 100);
728 assert_eq!(result.depth, 5);
729 }
730
731 #[test]
732 fn test_query_validator_creation() {
733 let config = ValidationConfig::default();
734 let schema = create_test_schema();
735 let validator = QueryValidator::new(config, schema);
736
737 assert_eq!(validator.config.max_depth, 10);
739 }
740
741 #[test]
742 fn test_validation_error_with_path() {
743 let error = ValidationError::new("Test error".to_string(), ValidationRule::FieldValidation)
744 .with_path(vec![
745 "query".to_string(),
746 "user".to_string(),
747 "name".to_string(),
748 ]);
749
750 assert_eq!(error.message, "Test error");
751 assert_eq!(error.path, vec!["query", "user", "name"]);
752 assert_eq!(error.rule, ValidationRule::FieldValidation);
753 }
754
755 #[test]
756 fn test_validation_warning_with_suggestion() {
757 let warning = ValidationWarning::new("Performance warning".to_string())
758 .with_suggestion("Consider using pagination".to_string());
759
760 assert_eq!(warning.message, "Performance warning");
761 assert_eq!(
762 warning.suggestion,
763 Some("Consider using pagination".to_string())
764 );
765 }
766
767 #[test]
768 fn test_rate_limit_config_default() {
769 let config = RateLimitConfig::default();
770 assert_eq!(config.max_queries_per_minute, 60);
771 assert_eq!(config.max_complexity_per_minute, 10000);
772 assert_eq!(config.window_duration, Duration::from_secs(60));
773 }
774
775 #[test]
776 fn test_validation_context() {
777 let schema = create_test_schema();
778 let mut context = ValidationContext::new(&schema);
779
780 let fragment = crate::ast::FragmentDefinition {
781 name: "TestFragment".to_string(),
782 type_condition: "Query".to_string(),
783 selection_set: crate::ast::SelectionSet { selections: vec![] },
784 directives: vec![],
785 };
786
787 context.add_fragment("TestFragment".to_string(), fragment.clone());
788
789 let retrieved = context.get_fragment("TestFragment");
790 assert!(retrieved.is_some());
791 assert_eq!(retrieved.expect("should succeed").name, "TestFragment");
792 }
793}