1use serde::{Deserialize, Serialize};
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ConfigSnapshot {
22 pub rate_limit: RateLimitSection,
23 pub request_log: RequestLogSection,
24 pub auth: AuthSection,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct RateLimitSection {
30 pub max_requests: u32,
31 pub window_secs: u64,
32 pub enabled: bool,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct RequestLogSection {
38 pub capacity: usize,
39 pub enabled: bool,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct AuthSection {
45 pub enabled: bool,
46 pub active_keys: usize,
47 pub total_keys: usize,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize, Default)]
55pub struct ConfigUpdate {
56 pub rate_limit: Option<RateLimitUpdate>,
57 pub request_log: Option<RequestLogUpdate>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, Default)]
62pub struct RateLimitUpdate {
63 pub max_requests: Option<u32>,
64 pub window_secs: Option<u64>,
65 pub enabled: Option<bool>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, Default)]
70pub struct RequestLogUpdate {
71 pub capacity: Option<usize>,
72 pub enabled: Option<bool>,
73}
74
75#[derive(Debug, Clone, Serialize)]
79pub struct ConfigChange {
80 pub section: String,
81 pub field: String,
82 pub old_value: String,
83 pub new_value: String,
84}
85
86#[derive(Debug, Clone, Serialize)]
88pub struct ConfigUpdateResult {
89 pub applied: bool,
90 pub changes: Vec<ConfigChange>,
91 pub snapshot: ConfigSnapshot,
92}
93
94pub fn snapshot(
98 rate_limiter: &crate::rate_limiter::RateLimiter,
99 request_logger: &crate::request_log::RequestLogger,
100 api_keys: &crate::api_keys::ApiKeyManager,
101) -> ConfigSnapshot {
102 let rl = rate_limiter.config();
103 let log = request_logger.config();
104
105 ConfigSnapshot {
106 rate_limit: RateLimitSection {
107 max_requests: rl.max_requests,
108 window_secs: rl.window.as_secs(),
109 enabled: rl.enabled,
110 },
111 request_log: RequestLogSection {
112 capacity: log.capacity,
113 enabled: log.enabled,
114 },
115 auth: AuthSection {
116 enabled: api_keys.is_enabled(),
117 active_keys: api_keys.active_count(),
118 total_keys: api_keys.total_count(),
119 },
120 }
121}
122
123pub fn snapshot_with_auth(
125 rate_limiter: &crate::rate_limiter::RateLimiter,
126 request_logger: &crate::request_log::RequestLogger,
127 auth: &AuthSection,
128) -> ConfigSnapshot {
129 let rl = rate_limiter.config();
130 let log = request_logger.config();
131
132 ConfigSnapshot {
133 rate_limit: RateLimitSection {
134 max_requests: rl.max_requests,
135 window_secs: rl.window.as_secs(),
136 enabled: rl.enabled,
137 },
138 request_log: RequestLogSection {
139 capacity: log.capacity,
140 enabled: log.enabled,
141 },
142 auth: auth.clone(),
143 }
144}
145
146pub fn apply_rate_limit(
150 update: &RateLimitUpdate,
151 rate_limiter: &mut crate::rate_limiter::RateLimiter,
152) -> Vec<ConfigChange> {
153 let mut changes = Vec::new();
154 let old = rate_limiter.config().clone();
155
156 if let Some(max) = update.max_requests {
157 if max != old.max_requests {
158 changes.push(ConfigChange {
159 section: "rate_limit".into(),
160 field: "max_requests".into(),
161 old_value: old.max_requests.to_string(),
162 new_value: max.to_string(),
163 });
164 }
165 }
166 if let Some(secs) = update.window_secs {
167 if secs != old.window.as_secs() {
168 changes.push(ConfigChange {
169 section: "rate_limit".into(),
170 field: "window_secs".into(),
171 old_value: old.window.as_secs().to_string(),
172 new_value: secs.to_string(),
173 });
174 }
175 }
176 if let Some(en) = update.enabled {
177 if en != old.enabled {
178 changes.push(ConfigChange {
179 section: "rate_limit".into(),
180 field: "enabled".into(),
181 old_value: old.enabled.to_string(),
182 new_value: en.to_string(),
183 });
184 }
185 }
186
187 rate_limiter.update_config(update.max_requests, update.window_secs, update.enabled);
188 changes
189}
190
191pub fn apply_request_log(
193 update: &RequestLogUpdate,
194 request_logger: &mut crate::request_log::RequestLogger,
195) -> Vec<ConfigChange> {
196 let mut changes = Vec::new();
197 let old = request_logger.config().clone();
198
199 if let Some(cap) = update.capacity {
200 if cap != old.capacity {
201 changes.push(ConfigChange {
202 section: "request_log".into(),
203 field: "capacity".into(),
204 old_value: old.capacity.to_string(),
205 new_value: cap.to_string(),
206 });
207 }
208 }
209 if let Some(en) = update.enabled {
210 if en != old.enabled {
211 changes.push(ConfigChange {
212 section: "request_log".into(),
213 field: "enabled".into(),
214 old_value: old.enabled.to_string(),
215 new_value: en.to_string(),
216 });
217 }
218 }
219
220 request_logger.update_config(update.capacity, update.enabled);
221 changes
222}
223
224pub fn apply(
227 update: &ConfigUpdate,
228 rate_limiter: &mut crate::rate_limiter::RateLimiter,
229 request_logger: &mut crate::request_log::RequestLogger,
230 auth_snap: &AuthSection,
231) -> ConfigUpdateResult {
232 let mut changes = Vec::new();
233
234 if let Some(ref rl) = update.rate_limit {
235 changes.extend(apply_rate_limit(rl, rate_limiter));
236 }
237 if let Some(ref log) = update.request_log {
238 changes.extend(apply_request_log(log, request_logger));
239 }
240
241 let new_snapshot = snapshot_with_auth(rate_limiter, request_logger, auth_snap);
242
243 ConfigUpdateResult {
244 applied: !changes.is_empty(),
245 changes,
246 snapshot: new_snapshot,
247 }
248}
249
250#[cfg(test)]
253mod tests {
254 use super::*;
255 use crate::api_keys::ApiKeyManager;
256 use crate::rate_limiter::{RateLimiter, RateLimitConfig};
257 use crate::request_log::{RequestLogger, RequestLogConfig};
258
259 fn make_components() -> (RateLimiter, RequestLogger, AuthSection) {
260 let rl = RateLimiter::new(RateLimitConfig::default_config());
261 let log = RequestLogger::new(RequestLogConfig::default_config());
262 let auth = AuthSection { enabled: false, active_keys: 0, total_keys: 0 };
263 (rl, log, auth)
264 }
265
266 #[test]
267 fn snapshot_captures_defaults() {
268 let (rl, log, auth) = make_components();
269 let snap = snapshot_with_auth(&rl, &log, &auth);
270
271 assert_eq!(snap.rate_limit.max_requests, 100);
272 assert_eq!(snap.rate_limit.window_secs, 60);
273 assert!(snap.rate_limit.enabled);
274 assert_eq!(snap.request_log.capacity, 1000);
275 assert!(snap.request_log.enabled);
276 assert!(!snap.auth.enabled);
277 assert_eq!(snap.auth.active_keys, 0);
278 }
279
280 #[test]
281 fn snapshot_with_auth_enabled() {
282 let rl = RateLimiter::new(RateLimitConfig::default_config());
283 let log = RequestLogger::new(RequestLogConfig::default_config());
284 let keys = ApiKeyManager::new(Some("master_tok"));
285 let auth = AuthSection {
286 enabled: keys.is_enabled(),
287 active_keys: keys.active_count(),
288 total_keys: keys.total_count(),
289 };
290
291 let snap = snapshot_with_auth(&rl, &log, &auth);
292 assert!(snap.auth.enabled);
293 assert_eq!(snap.auth.active_keys, 1);
294 assert_eq!(snap.auth.total_keys, 1);
295 }
296
297 #[test]
298 fn apply_rate_limit_changes() {
299 let (mut rl, mut log, auth) = make_components();
300
301 let update = ConfigUpdate {
302 rate_limit: Some(RateLimitUpdate {
303 max_requests: Some(200),
304 window_secs: Some(120),
305 enabled: None,
306 }),
307 request_log: None,
308 };
309
310 let result = apply(&update, &mut rl, &mut log, &auth);
311 assert!(result.applied);
312 assert_eq!(result.changes.len(), 2);
313 assert_eq!(result.snapshot.rate_limit.max_requests, 200);
314 assert_eq!(result.snapshot.rate_limit.window_secs, 120);
315 assert!(result.snapshot.rate_limit.enabled); }
317
318 #[test]
319 fn apply_request_log_changes() {
320 let (mut rl, mut log, auth) = make_components();
321
322 let update = ConfigUpdate {
323 rate_limit: None,
324 request_log: Some(RequestLogUpdate {
325 capacity: Some(500),
326 enabled: Some(false),
327 }),
328 };
329
330 let result = apply(&update, &mut rl, &mut log, &auth);
331 assert!(result.applied);
332 assert_eq!(result.changes.len(), 2);
333 assert_eq!(result.snapshot.request_log.capacity, 500);
334 assert!(!result.snapshot.request_log.enabled);
335 }
336
337 #[test]
338 fn apply_no_changes_when_same_values() {
339 let (mut rl, mut log, auth) = make_components();
340
341 let update = ConfigUpdate {
342 rate_limit: Some(RateLimitUpdate {
343 max_requests: Some(100), window_secs: Some(60), enabled: None,
346 }),
347 request_log: None,
348 };
349
350 let result = apply(&update, &mut rl, &mut log, &auth);
351 assert!(!result.applied);
352 assert!(result.changes.is_empty());
353 }
354
355 #[test]
356 fn apply_empty_update() {
357 let (mut rl, mut log, auth) = make_components();
358
359 let update = ConfigUpdate::default();
360 let result = apply(&update, &mut rl, &mut log, &auth);
361 assert!(!result.applied);
362 assert!(result.changes.is_empty());
363 }
364
365 #[test]
366 fn apply_combined_changes() {
367 let (mut rl, mut log, auth) = make_components();
368
369 let update = ConfigUpdate {
370 rate_limit: Some(RateLimitUpdate {
371 max_requests: Some(50),
372 window_secs: None,
373 enabled: Some(false),
374 }),
375 request_log: Some(RequestLogUpdate {
376 capacity: Some(2000),
377 enabled: None,
378 }),
379 };
380
381 let result = apply(&update, &mut rl, &mut log, &auth);
382 assert!(result.applied);
383 assert_eq!(result.changes.len(), 3);
384 assert_eq!(result.snapshot.rate_limit.max_requests, 50);
385 assert!(!result.snapshot.rate_limit.enabled);
386 assert_eq!(result.snapshot.request_log.capacity, 2000);
387 }
388
389 #[test]
390 fn change_tracking_records_old_and_new() {
391 let (mut rl, mut log, auth) = make_components();
392
393 let update = ConfigUpdate {
394 rate_limit: Some(RateLimitUpdate {
395 max_requests: Some(250),
396 window_secs: None,
397 enabled: None,
398 }),
399 request_log: None,
400 };
401
402 let result = apply(&update, &mut rl, &mut log, &auth);
403 assert_eq!(result.changes.len(), 1);
404 let c = &result.changes[0];
405 assert_eq!(c.section, "rate_limit");
406 assert_eq!(c.field, "max_requests");
407 assert_eq!(c.old_value, "100");
408 assert_eq!(c.new_value, "250");
409 }
410
411 #[test]
412 fn snapshot_serializes_to_json() {
413 let (rl, log, auth) = make_components();
414 let snap = snapshot_with_auth(&rl, &log, &auth);
415 let json = serde_json::to_value(&snap).unwrap();
416
417 assert_eq!(json["rate_limit"]["max_requests"], 100);
418 assert_eq!(json["rate_limit"]["window_secs"], 60);
419 assert_eq!(json["request_log"]["capacity"], 1000);
420 assert_eq!(json["auth"]["enabled"], false);
421 }
422
423 #[test]
424 fn update_result_serializes() {
425 let (mut rl, mut log, auth) = make_components();
426 let update = ConfigUpdate {
427 rate_limit: Some(RateLimitUpdate {
428 max_requests: Some(75),
429 window_secs: None,
430 enabled: None,
431 }),
432 request_log: None,
433 };
434
435 let result = apply(&update, &mut rl, &mut log, &auth);
436 let json = serde_json::to_value(&result).unwrap();
437 assert_eq!(json["applied"], true);
438 assert!(json["changes"].as_array().unwrap().len() == 1);
439 assert_eq!(json["snapshot"]["rate_limit"]["max_requests"], 75);
440 }
441
442 #[test]
443 fn disable_then_reenable_rate_limit() {
444 let (mut rl, mut log, auth) = make_components();
445
446 let update = ConfigUpdate {
448 rate_limit: Some(RateLimitUpdate {
449 enabled: Some(false),
450 ..Default::default()
451 }),
452 request_log: None,
453 };
454 let result = apply(&update, &mut rl, &mut log, &auth);
455 assert!(!result.snapshot.rate_limit.enabled);
456
457 let update = ConfigUpdate {
459 rate_limit: Some(RateLimitUpdate {
460 enabled: Some(true),
461 max_requests: Some(500),
462 ..Default::default()
463 }),
464 request_log: None,
465 };
466 let result = apply(&update, &mut rl, &mut log, &auth);
467 assert!(result.snapshot.rate_limit.enabled);
468 assert_eq!(result.snapshot.rate_limit.max_requests, 500);
469 }
470}