fraiseql_core/graphql/
complexity.rs1#[derive(Debug, Clone)]
6pub struct ComplexityConfig {
7 pub max_depth: usize,
9 pub max_fields: usize,
11 pub max_score: usize,
13}
14
15impl Default for ComplexityConfig {
16 fn default() -> Self {
17 Self {
18 max_depth: 15,
19 max_fields: 100,
20 max_score: 500,
21 }
22 }
23}
24
25pub struct ComplexityAnalyzer {
27 config: ComplexityConfig,
28}
29
30impl ComplexityAnalyzer {
31 #[must_use]
33 pub fn new() -> Self {
34 Self {
35 config: ComplexityConfig::default(),
36 }
37 }
38
39 #[must_use]
41 pub fn with_config(config: ComplexityConfig) -> Self {
42 Self { config }
43 }
44
45 #[must_use]
48 pub fn analyze_complexity(&self, query: &str) -> (usize, usize, usize) {
49 let mut max_depth = 0;
51 let mut current_depth = 0;
52 let mut field_count = 0;
53 let mut in_braces = false;
54
55 for ch in query.chars() {
56 match ch {
57 '{' => {
58 in_braces = true;
59 current_depth += 1;
60 max_depth = max_depth.max(current_depth);
61 },
62 '}' => {
63 if current_depth > 0 {
64 current_depth -= 1;
65 }
66 in_braces = false;
67 },
68 '(' | ')' => {
69 },
71 c if in_braces && c.is_alphabetic() => {
72 field_count += 1;
74 },
75 _ => {},
76 }
77 }
78
79 let total_score = max_depth * field_count.max(1);
80 (max_depth, field_count, total_score)
81 }
82
83 pub fn is_query_too_complex(&self, query: &str) -> Result<(), String> {
85 let (depth, fields, score) = self.analyze_complexity(query);
86
87 if depth > self.config.max_depth {
88 return Err(format!("Query depth {} exceeds maximum {}", depth, self.config.max_depth));
89 }
90
91 if fields > self.config.max_fields {
92 return Err(format!(
93 "Query field count {} exceeds maximum {}",
94 fields, self.config.max_fields
95 ));
96 }
97
98 if score > self.config.max_score {
99 return Err(format!(
100 "Query complexity score {} exceeds maximum {}",
101 score, self.config.max_score
102 ));
103 }
104
105 Ok(())
106 }
107}
108
109impl Default for ComplexityAnalyzer {
110 fn default() -> Self {
111 Self::new()
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn test_simple_query_complexity() {
121 let analyzer = ComplexityAnalyzer::new();
122 let query = "{ users { id name } }";
123 let (depth, _fields, _score) = analyzer.analyze_complexity(query);
124 assert!(depth <= 3);
125 }
126
127 #[test]
128 fn test_deeply_nested_query() {
129 let analyzer = ComplexityAnalyzer::new();
130 let query = "{ a { b { c { d { e { f { g { h } } } } } } } }";
131 let (depth, _fields, _score) = analyzer.analyze_complexity(query);
132 assert!(depth >= 8);
133 }
134
135 #[test]
136 fn test_query_too_deep() {
137 let config = ComplexityConfig {
138 max_depth: 5,
139 max_fields: 100,
140 max_score: 500,
141 };
142 let analyzer = ComplexityAnalyzer::with_config(config);
143
144 let query = "{ a { b { c { d { e { f { g { h } } } } } } } }";
145 assert!(analyzer.is_query_too_complex(query).is_err());
146 }
147
148 #[test]
149 fn test_query_within_limits() {
150 let analyzer = ComplexityAnalyzer::new();
151 let query = "{ users { id name email } posts { id title } }";
152 assert!(analyzer.is_query_too_complex(query).is_ok());
153 }
154
155 #[test]
156 fn test_complexity_score() {
157 let analyzer = ComplexityAnalyzer::new();
158 let query = "{ users { id name email } }";
159 let (_depth, _fields, score) = analyzer.analyze_complexity(query);
160 assert!(score <= 500);
162 }
163}