1use graphql_parser::query::{
8 Definition, Document, FragmentDefinition, OperationDefinition, Selection, SelectionSet,
9};
10
11pub fn parse_graphql_document(
24 query: &str,
25) -> Result<Document<'_, String>, ComplexityValidationError> {
26 if query.trim().is_empty() {
27 return Err(ComplexityValidationError::MalformedQuery("Empty query".to_string()));
28 }
29 graphql_parser::parse_query::<String>(query)
30 .map_err(|e| ComplexityValidationError::MalformedQuery(format!("{e}")))
31}
32
33pub const DEFAULT_MAX_ALIASES: usize = 30;
38
39pub const MAX_VARIABLES_COUNT: usize = 1_000;
45
46#[derive(Debug, Clone)]
48pub struct ComplexityConfig {
49 pub max_depth: usize,
51 pub max_complexity: usize,
53 pub max_aliases: usize,
55}
56
57impl Default for ComplexityConfig {
58 fn default() -> Self {
59 Self {
60 max_depth: 10,
61 max_complexity: 100,
62 max_aliases: DEFAULT_MAX_ALIASES,
63 }
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct QueryMetrics {
70 pub depth: usize,
72 pub complexity: usize,
74 pub alias_count: usize,
76}
77
78#[derive(Debug, thiserror::Error, Clone)]
80#[non_exhaustive]
81pub enum ComplexityValidationError {
82 #[error("Query exceeds maximum depth of {max_depth}: depth = {actual_depth}")]
84 QueryTooDeep {
85 max_depth: usize,
87 actual_depth: usize,
89 },
90
91 #[error("Query exceeds maximum complexity of {max_complexity}: score = {actual_complexity}")]
93 QueryTooComplex {
94 max_complexity: usize,
96 actual_complexity: usize,
98 },
99
100 #[error("Query exceeds maximum alias count of {max_aliases}: count = {actual_aliases}")]
102 TooManyAliases {
103 max_aliases: usize,
105 actual_aliases: usize,
107 },
108
109 #[error("Invalid variables: {0}")]
111 InvalidVariables(String),
112
113 #[error("Malformed GraphQL query: {0}")]
115 MalformedQuery(String),
116}
117
118#[derive(Debug, Clone)]
124pub struct RequestValidator {
125 max_depth: usize,
127 max_complexity: usize,
129 max_aliases_per_query: usize,
131 validate_depth: bool,
133 validate_complexity: bool,
135}
136
137impl RequestValidator {
138 #[must_use]
140 pub fn new() -> Self {
141 Self::default()
142 }
143
144 #[must_use]
146 pub const fn from_config(config: &ComplexityConfig) -> Self {
147 Self {
148 max_depth: config.max_depth,
149 max_complexity: config.max_complexity,
150 max_aliases_per_query: config.max_aliases,
151 validate_depth: true,
152 validate_complexity: true,
153 }
154 }
155
156 #[must_use]
158 pub const fn with_max_depth(mut self, max_depth: usize) -> Self {
159 self.max_depth = max_depth;
160 self
161 }
162
163 #[must_use]
165 pub const fn with_max_complexity(mut self, max_complexity: usize) -> Self {
166 self.max_complexity = max_complexity;
167 self
168 }
169
170 #[must_use]
172 pub const fn with_depth_validation(mut self, enabled: bool) -> Self {
173 self.validate_depth = enabled;
174 self
175 }
176
177 #[must_use]
179 pub const fn with_complexity_validation(mut self, enabled: bool) -> Self {
180 self.validate_complexity = enabled;
181 self
182 }
183
184 #[must_use]
186 pub const fn with_max_aliases(mut self, max_aliases: usize) -> Self {
187 self.max_aliases_per_query = max_aliases;
188 self
189 }
190
191 pub fn analyze(&self, query: &str) -> Result<QueryMetrics, ComplexityValidationError> {
201 if query.trim().is_empty() {
202 return Err(ComplexityValidationError::MalformedQuery("Empty query".to_string()));
203 }
204 let document = graphql_parser::parse_query::<String>(query)
205 .map_err(|e| ComplexityValidationError::MalformedQuery(format!("{e}")))?;
206 let fragments = collect_fragments(&document);
207 Ok(QueryMetrics {
208 depth: self.calculate_depth_ast(&document, &fragments),
209 complexity: self.calculate_complexity_ast(&document, &fragments),
210 alias_count: self.count_aliases_ast(&document),
211 })
212 }
213
214 #[must_use]
222 pub const fn is_no_op(&self) -> bool {
223 !self.validate_depth && !self.validate_complexity && self.max_aliases_per_query == 0
224 }
225
226 pub fn validate_query(&self, query: &str) -> Result<(), ComplexityValidationError> {
232 if query.trim().is_empty() {
233 return Err(ComplexityValidationError::MalformedQuery("Empty query".to_string()));
234 }
235
236 if self.is_no_op() {
237 return Ok(());
238 }
239
240 let document = graphql_parser::parse_query::<String>(query)
241 .map_err(|e| ComplexityValidationError::MalformedQuery(format!("{e}")))?;
242 self.validate_query_doc(&document)
243 }
244
245 pub fn validate_query_doc<'a>(
257 &self,
258 document: &'a Document<'a, String>,
259 ) -> Result<(), ComplexityValidationError> {
260 if self.is_no_op() {
261 return Ok(());
262 }
263
264 let fragments = collect_fragments(document);
265
266 if self.validate_depth {
267 let depth = self.calculate_depth_ast(document, &fragments);
268 if depth > self.max_depth {
269 return Err(ComplexityValidationError::QueryTooDeep {
270 max_depth: self.max_depth,
271 actual_depth: depth,
272 });
273 }
274 }
275
276 if self.validate_complexity {
277 let complexity = self.calculate_complexity_ast(document, &fragments);
278 if complexity > self.max_complexity {
279 return Err(ComplexityValidationError::QueryTooComplex {
280 max_complexity: self.max_complexity,
281 actual_complexity: complexity,
282 });
283 }
284 }
285
286 let alias_count = self.count_aliases_ast(document);
287 if alias_count > self.max_aliases_per_query {
288 return Err(ComplexityValidationError::TooManyAliases {
289 max_aliases: self.max_aliases_per_query,
290 actual_aliases: alias_count,
291 });
292 }
293
294 Ok(())
295 }
296
297 pub fn validate_variables(
309 &self,
310 variables: Option<&serde_json::Value>,
311 ) -> Result<(), ComplexityValidationError> {
312 if let Some(vars) = variables {
313 if !vars.is_object() {
314 return Err(ComplexityValidationError::InvalidVariables(
315 "Variables must be an object".to_string(),
316 ));
317 }
318 let obj = vars.as_object().expect("invariant: vars.is_object() checked above");
321 if obj.len() > MAX_VARIABLES_COUNT {
322 return Err(ComplexityValidationError::InvalidVariables(format!(
323 "Too many variables: {} exceeds maximum of {}",
324 obj.len(),
325 MAX_VARIABLES_COUNT
326 )));
327 }
328 }
329 Ok(())
330 }
331
332 fn calculate_depth_ast(
333 &self,
334 document: &Document<String>,
335 fragments: &[&FragmentDefinition<String>],
336 ) -> usize {
337 document
338 .definitions
339 .iter()
340 .map(|def| match def {
341 Definition::Operation(op) => match op {
342 OperationDefinition::Query(q) => {
343 self.selection_set_depth(&q.selection_set, fragments, 0)
344 },
345 OperationDefinition::Mutation(m) => {
346 self.selection_set_depth(&m.selection_set, fragments, 0)
347 },
348 OperationDefinition::Subscription(s) => {
349 self.selection_set_depth(&s.selection_set, fragments, 0)
350 },
351 OperationDefinition::SelectionSet(ss) => {
352 self.selection_set_depth(ss, fragments, 0)
353 },
354 },
355 Definition::Fragment(f) => self.selection_set_depth(&f.selection_set, fragments, 0),
356 })
357 .max()
358 .unwrap_or(0)
359 }
360
361 fn selection_set_depth(
362 &self,
363 selection_set: &SelectionSet<String>,
364 fragments: &[&FragmentDefinition<String>],
365 recursion_depth: usize,
366 ) -> usize {
367 if recursion_depth > 32 {
368 return self.max_depth + 1;
369 }
370 if selection_set.items.is_empty() {
371 return 0;
372 }
373 let max_child = selection_set
374 .items
375 .iter()
376 .map(|sel| match sel {
377 Selection::Field(field) => {
378 if field.selection_set.items.is_empty() {
379 0
380 } else {
381 self.selection_set_depth(&field.selection_set, fragments, recursion_depth)
382 }
383 },
384 Selection::InlineFragment(inline) => {
385 self.selection_set_depth(&inline.selection_set, fragments, recursion_depth)
386 },
387 Selection::FragmentSpread(spread) => {
388 if let Some(frag) = fragments.iter().find(|f| f.name == spread.fragment_name) {
389 self.selection_set_depth(
390 &frag.selection_set,
391 fragments,
392 recursion_depth + 1,
393 )
394 } else {
395 self.max_depth
396 }
397 },
398 })
399 .max()
400 .unwrap_or(0);
401 1 + max_child
402 }
403
404 fn calculate_complexity_ast(
405 &self,
406 document: &Document<String>,
407 fragments: &[&FragmentDefinition<String>],
408 ) -> usize {
409 document
410 .definitions
411 .iter()
412 .map(|def| match def {
413 Definition::Operation(op) => match op {
414 OperationDefinition::Query(q) => {
415 self.selection_set_complexity(&q.selection_set, fragments, 0)
416 },
417 OperationDefinition::Mutation(m) => {
418 self.selection_set_complexity(&m.selection_set, fragments, 0)
419 },
420 OperationDefinition::Subscription(s) => {
421 self.selection_set_complexity(&s.selection_set, fragments, 0)
422 },
423 OperationDefinition::SelectionSet(ss) => {
424 self.selection_set_complexity(ss, fragments, 0)
425 },
426 },
427 Definition::Fragment(_) => 0,
428 })
429 .sum()
430 }
431
432 fn selection_set_complexity(
433 &self,
434 selection_set: &SelectionSet<String>,
435 fragments: &[&FragmentDefinition<String>],
436 recursion_depth: usize,
437 ) -> usize {
438 if recursion_depth > 32 {
439 return self.max_complexity + 1;
440 }
441 selection_set
442 .items
443 .iter()
444 .map(|sel| match sel {
445 Selection::Field(field) => {
446 let multiplier = extract_limit_multiplier(&field.arguments);
447 if field.selection_set.items.is_empty() {
448 1
449 } else {
450 let nested = self.selection_set_complexity(
451 &field.selection_set,
452 fragments,
453 recursion_depth,
454 );
455 1 + nested * multiplier
456 }
457 },
458 Selection::InlineFragment(inline) => {
459 self.selection_set_complexity(&inline.selection_set, fragments, recursion_depth)
460 },
461 Selection::FragmentSpread(spread) => {
462 if let Some(frag) = fragments.iter().find(|f| f.name == spread.fragment_name) {
463 self.selection_set_complexity(
464 &frag.selection_set,
465 fragments,
466 recursion_depth + 1,
467 )
468 } else {
469 10
470 }
471 },
472 })
473 .sum()
474 }
475
476 fn count_aliases_ast(&self, document: &Document<String>) -> usize {
477 document
478 .definitions
479 .iter()
480 .map(|def| match def {
481 Definition::Operation(op) => {
482 let ss = match op {
483 OperationDefinition::Query(q) => &q.selection_set,
484 OperationDefinition::Mutation(m) => &m.selection_set,
485 OperationDefinition::Subscription(s) => &s.selection_set,
486 OperationDefinition::SelectionSet(ss) => ss,
487 };
488 count_aliases_in_selection_set(ss)
489 },
490 Definition::Fragment(f) => count_aliases_in_selection_set(&f.selection_set),
491 })
492 .sum()
493 }
494}
495
496impl Default for RequestValidator {
497 fn default() -> Self {
498 Self {
499 max_depth: 10,
500 max_complexity: 100,
501 max_aliases_per_query: DEFAULT_MAX_ALIASES,
502 validate_depth: true,
503 validate_complexity: true,
504 }
505 }
506}
507
508fn collect_fragments<'a>(
510 document: &'a Document<'a, String>,
511) -> Vec<&'a FragmentDefinition<'a, String>> {
512 document
513 .definitions
514 .iter()
515 .filter_map(|def| {
516 if let Definition::Fragment(f) = def {
517 Some(f)
518 } else {
519 None
520 }
521 })
522 .collect()
523}
524
525fn extract_limit_multiplier(arguments: &[(String, graphql_parser::query::Value<String>)]) -> usize {
527 for (name, value) in arguments {
528 if matches!(name.as_str(), "first" | "limit" | "take" | "last") {
529 if let graphql_parser::query::Value::Int(n) = value {
530 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
531 let limit = n.as_i64().unwrap_or(10) as usize;
534 return limit.clamp(1, 100);
535 }
536 }
537 }
538 1
539}
540
541fn count_aliases_in_selection_set(selection_set: &SelectionSet<String>) -> usize {
543 selection_set
544 .items
545 .iter()
546 .map(|sel| match sel {
547 Selection::Field(field) => {
548 let self_alias = usize::from(field.alias.is_some());
549 self_alias + count_aliases_in_selection_set(&field.selection_set)
550 },
551 Selection::InlineFragment(inline) => {
552 count_aliases_in_selection_set(&inline.selection_set)
553 },
554 Selection::FragmentSpread(_) => 0,
555 })
556 .sum()
557}