fraiseql_core/validation/
rate_limiting.rs1use std::{
11 collections::HashMap,
12 sync::{Arc, Mutex},
13 time::{SystemTime, UNIX_EPOCH},
14};
15
16use crate::error::FraiseQLError;
17
18#[derive(Debug, Clone)]
20pub struct RateLimitDimension {
21 pub max_requests: u32,
23 pub window_secs: u64,
25}
26
27impl RateLimitDimension {
28 fn is_rate_limited(&self) -> bool {
29 self.max_requests == 0
30 }
31}
32
33#[derive(Debug, Clone)]
35pub struct ValidationRateLimitingConfig {
36 pub enabled: bool,
38 pub validation_errors_max_requests: u32,
40 pub validation_errors_window_secs: u64,
42 pub depth_errors_max_requests: u32,
44 pub depth_errors_window_secs: u64,
46 pub complexity_errors_max_requests: u32,
48 pub complexity_errors_window_secs: u64,
50 pub malformed_errors_max_requests: u32,
52 pub malformed_errors_window_secs: u64,
54 pub async_validation_errors_max_requests: u32,
56 pub async_validation_errors_window_secs: u64,
58}
59
60impl Default for ValidationRateLimitingConfig {
61 fn default() -> Self {
62 Self {
63 enabled: true,
64 validation_errors_max_requests: 100,
65 validation_errors_window_secs: 60,
66 depth_errors_max_requests: 50,
67 depth_errors_window_secs: 60,
68 complexity_errors_max_requests: 30,
69 complexity_errors_window_secs: 60,
70 malformed_errors_max_requests: 40,
71 malformed_errors_window_secs: 60,
72 async_validation_errors_max_requests: 60,
73 async_validation_errors_window_secs: 60,
74 }
75 }
76}
77
78#[derive(Debug, Clone)]
80struct RequestRecord {
81 count: u32,
83 window_start: u64,
85}
86
87struct DimensionRateLimiter {
89 records: Arc<Mutex<HashMap<String, RequestRecord>>>,
90 dimension: RateLimitDimension,
91}
92
93impl DimensionRateLimiter {
94 fn new(max_requests: u32, window_secs: u64) -> Self {
95 Self {
96 records: Arc::new(Mutex::new(HashMap::new())),
97 dimension: RateLimitDimension {
98 max_requests,
99 window_secs,
100 },
101 }
102 }
103
104 fn get_timestamp() -> u64 {
105 SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs()
106 }
107
108 fn check(&self, key: &str) -> Result<(), FraiseQLError> {
109 if self.dimension.is_rate_limited() {
110 return Ok(());
111 }
112
113 let mut records = self.records.lock().unwrap();
114 let now = Self::get_timestamp();
115
116 let record = records.entry(key.to_string()).or_insert_with(|| RequestRecord {
117 count: 0,
118 window_start: now,
119 });
120
121 if now >= record.window_start + self.dimension.window_secs {
123 record.count = 1;
125 record.window_start = now;
126 Ok(())
127 } else if record.count < self.dimension.max_requests {
128 record.count += 1;
130 Ok(())
131 } else {
132 Err(FraiseQLError::RateLimited {
134 message: "Rate limit exceeded for validation errors".to_string(),
135 retry_after_secs: self.dimension.window_secs,
136 })
137 }
138 }
139
140 fn clear(&self) {
141 let mut records = self.records.lock().unwrap();
142 records.clear();
143 }
144}
145
146impl Clone for DimensionRateLimiter {
147 fn clone(&self) -> Self {
148 Self {
149 records: Arc::clone(&self.records),
150 dimension: self.dimension.clone(),
151 }
152 }
153}
154
155#[derive(Clone)]
157#[allow(clippy::module_name_repetitions, clippy::struct_field_names)] pub struct ValidationRateLimiter {
159 validation_errors: DimensionRateLimiter,
160 depth_errors: DimensionRateLimiter,
161 complexity_errors: DimensionRateLimiter,
162 malformed_errors: DimensionRateLimiter,
163 async_validation_errors: DimensionRateLimiter,
164}
165
166impl ValidationRateLimiter {
167 pub fn new(config: ValidationRateLimitingConfig) -> Self {
169 Self {
170 validation_errors: DimensionRateLimiter::new(
171 config.validation_errors_max_requests,
172 config.validation_errors_window_secs,
173 ),
174 depth_errors: DimensionRateLimiter::new(
175 config.depth_errors_max_requests,
176 config.depth_errors_window_secs,
177 ),
178 complexity_errors: DimensionRateLimiter::new(
179 config.complexity_errors_max_requests,
180 config.complexity_errors_window_secs,
181 ),
182 malformed_errors: DimensionRateLimiter::new(
183 config.malformed_errors_max_requests,
184 config.malformed_errors_window_secs,
185 ),
186 async_validation_errors: DimensionRateLimiter::new(
187 config.async_validation_errors_max_requests,
188 config.async_validation_errors_window_secs,
189 ),
190 }
191 }
192
193 pub fn check_validation_errors(&self, key: &str) -> Result<(), FraiseQLError> {
195 self.validation_errors.check(key)
196 }
197
198 pub fn check_depth_errors(&self, key: &str) -> Result<(), FraiseQLError> {
200 self.depth_errors.check(key)
201 }
202
203 pub fn check_complexity_errors(&self, key: &str) -> Result<(), FraiseQLError> {
205 self.complexity_errors.check(key)
206 }
207
208 pub fn check_malformed_errors(&self, key: &str) -> Result<(), FraiseQLError> {
210 self.malformed_errors.check(key)
211 }
212
213 pub fn check_async_validation_errors(&self, key: &str) -> Result<(), FraiseQLError> {
215 self.async_validation_errors.check(key)
216 }
217
218 pub fn clear(&self) {
220 self.validation_errors.clear();
221 self.depth_errors.clear();
222 self.complexity_errors.clear();
223 self.malformed_errors.clear();
224 self.async_validation_errors.clear();
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn test_dimension_rate_limiter_allows_within_limit() {
234 let limiter = DimensionRateLimiter::new(3, 60);
235 assert!(limiter.check("key").is_ok());
236 assert!(limiter.check("key").is_ok());
237 assert!(limiter.check("key").is_ok());
238 }
239
240 #[test]
241 fn test_dimension_rate_limiter_rejects_over_limit() {
242 let limiter = DimensionRateLimiter::new(2, 60);
243 assert!(limiter.check("key").is_ok());
244 assert!(limiter.check("key").is_ok());
245 assert!(limiter.check("key").is_err());
246 }
247
248 #[test]
249 fn test_dimension_rate_limiter_per_key() {
250 let limiter = DimensionRateLimiter::new(2, 60);
251 assert!(limiter.check("key1").is_ok());
252 assert!(limiter.check("key1").is_ok());
253 assert!(limiter.check("key2").is_ok());
254 }
255
256 #[test]
257 fn test_dimension_rate_limiter_clear() {
258 let limiter = DimensionRateLimiter::new(1, 60);
259 assert!(limiter.check("key").is_ok());
260 assert!(limiter.check("key").is_err());
261 limiter.clear();
262 assert!(limiter.check("key").is_ok());
263 }
264
265 #[test]
266 fn test_config_defaults() {
267 let config = ValidationRateLimitingConfig::default();
268 assert!(config.enabled);
269 assert!(config.validation_errors_max_requests > 0);
270 assert!(config.depth_errors_max_requests > 0);
271 assert!(config.complexity_errors_max_requests > 0);
272 assert!(config.malformed_errors_max_requests > 0);
273 assert!(config.async_validation_errors_max_requests > 0);
274 }
275
276 #[test]
277 fn test_validation_limiter_independent_dimensions() {
278 let config = ValidationRateLimitingConfig::default();
279 let limiter = ValidationRateLimiter::new(config);
280 let key = "test-key";
281
282 for _ in 0..100 {
284 let _ = limiter.check_validation_errors(key);
285 }
286
287 assert!(limiter.check_validation_errors(key).is_err());
289
290 assert!(limiter.check_depth_errors(key).is_ok());
292 assert!(limiter.check_complexity_errors(key).is_ok());
293 assert!(limiter.check_malformed_errors(key).is_ok());
294 assert!(limiter.check_async_validation_errors(key).is_ok());
295 }
296
297 #[test]
298 fn test_validation_limiter_clone_shares_state() {
299 let config = ValidationRateLimitingConfig::default();
300 let limiter1 = ValidationRateLimiter::new(config);
301 let limiter2 = limiter1.clone();
302
303 let key = "shared-key";
304
305 for _ in 0..100 {
306 let _ = limiter1.check_validation_errors(key);
307 }
308
309 assert!(limiter2.check_validation_errors(key).is_err());
311 }
312}