fraiseql_server/
validation.rs1use serde_json::Value as JsonValue;
9use thiserror::Error;
10
11#[derive(Debug, Error, Clone)]
13pub enum ValidationError {
14 #[error("Query exceeds maximum depth of {max_depth}: depth = {actual_depth}")]
16 QueryTooDeep {
17 max_depth: usize,
19 actual_depth: usize,
21 },
22
23 #[error("Query exceeds maximum complexity of {max_complexity}: score = {actual_complexity}")]
25 QueryTooComplex {
26 max_complexity: usize,
28 actual_complexity: usize,
30 },
31
32 #[error("Invalid variables: {0}")]
34 InvalidVariables(String),
35
36 #[error("Malformed GraphQL query: {0}")]
38 MalformedQuery(String),
39}
40
41#[derive(Debug, Clone)]
43pub struct RequestValidator {
44 max_depth: usize,
46 max_complexity: usize,
48 validate_depth: bool,
50 validate_complexity: bool,
52}
53
54impl RequestValidator {
55 #[must_use]
57 pub fn new() -> Self {
58 Self::default()
59 }
60
61 #[must_use]
63 pub fn with_max_depth(mut self, max_depth: usize) -> Self {
64 self.max_depth = max_depth;
65 self
66 }
67
68 #[must_use]
70 pub fn with_max_complexity(mut self, max_complexity: usize) -> Self {
71 self.max_complexity = max_complexity;
72 self
73 }
74
75 #[must_use]
77 pub fn with_depth_validation(mut self, enabled: bool) -> Self {
78 self.validate_depth = enabled;
79 self
80 }
81
82 #[must_use]
84 pub fn with_complexity_validation(mut self, enabled: bool) -> Self {
85 self.validate_complexity = enabled;
86 self
87 }
88
89 pub fn validate_query(&self, query: &str) -> Result<(), ValidationError> {
95 if query.trim().is_empty() {
97 return Err(ValidationError::MalformedQuery("Empty query".to_string()));
98 }
99
100 if self.validate_depth {
102 let depth = self.calculate_depth(query);
103 if depth > self.max_depth {
104 return Err(ValidationError::QueryTooDeep {
105 max_depth: self.max_depth,
106 actual_depth: depth,
107 });
108 }
109 }
110
111 if self.validate_complexity {
113 let complexity = self.calculate_complexity(query);
114 if complexity > self.max_complexity {
115 return Err(ValidationError::QueryTooComplex {
116 max_complexity: self.max_complexity,
117 actual_complexity: complexity,
118 });
119 }
120 }
121
122 Ok(())
123 }
124
125 pub fn validate_variables(&self, variables: Option<&JsonValue>) -> Result<(), ValidationError> {
131 if let Some(vars) = variables {
132 if !vars.is_object() {
134 return Err(ValidationError::InvalidVariables(
135 "Variables must be an object".to_string(),
136 ));
137 }
138
139 }
142
143 Ok(())
144 }
145
146 fn calculate_depth(&self, query: &str) -> usize {
148 let mut max_depth: usize = 0;
149 let mut current_depth: usize = 0;
150 let mut in_string = false;
151 let mut escape_next = false;
152
153 for ch in query.chars() {
154 if escape_next {
155 escape_next = false;
156 continue;
157 }
158
159 if ch == '\\' && in_string {
160 escape_next = true;
161 continue;
162 }
163
164 if ch == '"' {
165 in_string = !in_string;
166 continue;
167 }
168
169 if in_string {
170 continue;
171 }
172
173 match ch {
174 '{' => {
175 current_depth += 1;
176 max_depth = max_depth.max(current_depth);
177 },
178 '}' => {
179 current_depth = current_depth.saturating_sub(1);
180 },
181 _ => {},
182 }
183 }
184
185 max_depth
186 }
187
188 fn calculate_complexity(&self, query: &str) -> usize {
190 let mut complexity = 0;
191 let mut in_string = false;
192 let mut escape_next = false;
193
194 for ch in query.chars() {
195 if escape_next {
196 escape_next = false;
197 continue;
198 }
199
200 if ch == '\\' && in_string {
201 escape_next = true;
202 continue;
203 }
204
205 if ch == '"' {
206 in_string = !in_string;
207 continue;
208 }
209
210 if in_string {
211 continue;
212 }
213
214 match ch {
215 '{' => complexity += 1,
216 '[' => complexity += 2, '(' => complexity += 1, _ => {},
219 }
220 }
221
222 complexity
223 }
224}
225
226impl Default for RequestValidator {
227 fn default() -> Self {
228 Self {
229 max_depth: 10,
230 max_complexity: 100,
231 validate_depth: true,
232 validate_complexity: true,
233 }
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn test_empty_query_validation() {
243 let validator = RequestValidator::new();
244 assert!(validator.validate_query("").is_err());
245 assert!(validator.validate_query(" ").is_err());
246 }
247
248 #[test]
249 fn test_query_depth_validation() {
250 let validator = RequestValidator::new().with_max_depth(3);
251
252 let shallow = "{ user { id } }";
254 assert!(validator.validate_query(shallow).is_ok());
255
256 let deep = "{ user { profile { settings { theme } } } }";
258 assert!(validator.validate_query(deep).is_err());
259 }
260
261 #[test]
262 fn test_query_complexity_validation() {
263 let validator = RequestValidator::new().with_max_complexity(5);
264
265 let simple = "{ user { id name } }";
267 assert!(validator.validate_query(simple).is_ok());
268
269 let complex = "{ user [ id name email [ tags [ name ] ] profile { bio avatar [ url size ] settings { theme notifications } } ] }";
271 assert!(validator.validate_query(complex).is_err());
272 }
273
274 #[test]
275 fn test_variables_validation() {
276 let validator = RequestValidator::new();
277
278 let valid = serde_json::json!({"id": "123", "name": "John"});
280 assert!(validator.validate_variables(Some(&valid)).is_ok());
281
282 assert!(validator.validate_variables(None).is_ok());
284
285 let invalid = serde_json::json!([1, 2, 3]);
287 assert!(validator.validate_variables(Some(&invalid)).is_err());
288 }
289
290 #[test]
291 fn test_depth_calculation_with_strings() {
292 let validator = RequestValidator::new();
293
294 let query = r#"{ user { description: "Has { and }" } }"#;
296 let depth = validator.calculate_depth(query);
297 assert_eq!(depth, 2);
298 }
299
300 #[test]
301 fn test_disable_validation() {
302 let validator = RequestValidator::new()
303 .with_depth_validation(false)
304 .with_complexity_validation(false)
305 .with_max_depth(1)
306 .with_max_complexity(1);
307
308 let deep = "{ a { b { c { d { e { f } } } } } }";
310 assert!(validator.validate_query(deep).is_ok());
311 }
312}