1use crate::{ConfigError, ConfigResult};
4use lazy_static::lazy_static;
5use regex::Regex;
6use serde::{Deserialize, Serialize};
7use validator::Validate;
8
9lazy_static! {
10 static ref REDIS_URL_REGEX: Regex = Regex::new(r"^rediss?://").unwrap();
11 static ref NEO4J_URL_REGEX: Regex = Regex::new(r"^(neo4j(\+s)?|bolt(\+s)?)://").unwrap();
12 static ref HTTP_URL_REGEX: Regex = Regex::new(r"^https?://").unwrap();
13 static ref POSTGRES_URL_REGEX: Regex = Regex::new(r"^postgres(ql)?://").unwrap();
14}
15
16fn _validate_redis_url(url: &str) -> Result<(), validator::ValidationError> {
18 if !REDIS_URL_REGEX.is_match(url) {
19 return Err(validator::ValidationError::new(
20 "REDIS_URL must start with redis:// or rediss://",
21 ));
22 }
23 if url::Url::parse(url).is_err() {
24 return Err(validator::ValidationError::new("Invalid Redis URL format"));
25 }
26 Ok(())
27}
28
29fn validate_redis_url_field(url: &str) -> Result<(), validator::ValidationError> {
31 _validate_redis_url(url)
32}
33
34#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
36pub struct DatabaseConfig {
37 #[validate(custom(function = "validate_redis_url_field"))]
39 pub redis_url: String,
40
41 #[serde(default)]
43 pub neo4j_url: Option<String>,
44
45 #[serde(default)]
47 pub neo4j_username: Option<String>,
48
49 #[serde(default)]
51 pub neo4j_password: Option<String>,
52
53 #[serde(default)]
55 pub clickhouse_url: Option<String>,
56
57 #[serde(default = "default_clickhouse_db")]
59 #[validate(length(min = 1))]
60 pub clickhouse_database: String,
61
62 #[serde(default)]
64 pub postgres_url: Option<String>,
65
66 #[serde(flatten)]
68 #[validate(nested)]
69 pub pool: PoolConfig,
70}
71
72#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
74pub struct PoolConfig {
75 #[serde(default = "default_max_connections")]
77 #[validate(range(min = 1, max = 1000))]
78 pub max_connections: u32,
79
80 #[serde(default = "default_min_connections")]
82 #[validate(range(min = 0, max = 100))]
83 pub min_connections: u32,
84
85 #[serde(default = "default_connection_timeout")]
87 #[validate(range(min = 1, max = 300))]
88 pub connection_timeout_secs: u64,
89
90 #[serde(default = "default_idle_timeout")]
92 pub idle_timeout_secs: u64,
93
94 #[serde(default = "default_max_lifetime")]
96 pub max_lifetime_secs: u64,
97}
98
99pub fn validate_neo4j_url(url: &str) -> ConfigResult<()> {
101 if !NEO4J_URL_REGEX.is_match(url) {
102 return Err(ConfigError::validation(
103 "NEO4J_URL must start with neo4j://, neo4j+s://, bolt://, or bolt+s://",
104 ));
105 }
106 if url::Url::parse(url).is_err() {
107 return Err(ConfigError::validation("Invalid Neo4j URL format"));
108 }
109 Ok(())
110}
111
112pub fn validate_clickhouse_url(url: &str) -> ConfigResult<()> {
114 if !HTTP_URL_REGEX.is_match(url) {
115 return Err(ConfigError::validation(
116 "CLICKHOUSE_URL must start with http:// or https://",
117 ));
118 }
119 if url::Url::parse(url).is_err() {
120 return Err(ConfigError::validation("Invalid ClickHouse URL format"));
121 }
122 Ok(())
123}
124
125pub fn validate_postgres_url(url: &str) -> ConfigResult<()> {
127 if !POSTGRES_URL_REGEX.is_match(url) {
128 return Err(ConfigError::validation(
129 "POSTGRES_URL must start with postgres:// or postgresql://",
130 ));
131 }
132 if url::Url::parse(url).is_err() {
133 return Err(ConfigError::validation("Invalid PostgreSQL URL format"));
134 }
135 Ok(())
136}
137
138impl DatabaseConfig {
139 pub fn validate_config(&self) -> ConfigResult<()> {
152 Validate::validate(self)
154 .map_err(|e| ConfigError::validation(format!("Validation failed: {}", e)))?;
155
156 if let Some(ref neo4j_url) = self.neo4j_url {
157 validate_neo4j_url(neo4j_url)?;
158 }
159
160 if let Some(ref clickhouse_url) = self.clickhouse_url {
161 validate_clickhouse_url(clickhouse_url)?;
162 }
163
164 if let Some(ref postgres_url) = self.postgres_url {
165 validate_postgres_url(postgres_url)?;
166 }
167
168 self.pool.validate_config()?;
169
170 Ok(())
171 }
172}
173
174impl PoolConfig {
175 pub fn validate_config(&self) -> ConfigResult<()> {
186 Validate::validate(self)
188 .map_err(|e| ConfigError::validation(format!("Validation failed: {}", e)))?;
189
190 if self.min_connections > self.max_connections {
192 return Err(ConfigError::validation(
193 "min_connections cannot be greater than max_connections",
194 ));
195 }
196
197 Ok(())
198 }
199}
200
201fn default_clickhouse_db() -> String {
203 "riglr".to_string()
204}
205fn default_max_connections() -> u32 {
206 10
207}
208fn default_min_connections() -> u32 {
209 2
210}
211fn default_connection_timeout() -> u64 {
212 30
213}
214fn default_idle_timeout() -> u64 {
215 600
216}
217fn default_max_lifetime() -> u64 {
218 3600
219}
220
221impl Default for DatabaseConfig {
222 fn default() -> Self {
223 Self {
224 redis_url: "redis://localhost:6379".to_string(),
225 neo4j_url: None,
226 neo4j_username: None,
227 neo4j_password: None,
228 clickhouse_url: None,
229 clickhouse_database: default_clickhouse_db(),
230 postgres_url: None,
231 pool: PoolConfig::default(),
232 }
233 }
234}
235
236impl Default for PoolConfig {
237 fn default() -> Self {
238 Self {
239 max_connections: default_max_connections(),
240 min_connections: default_min_connections(),
241 connection_timeout_secs: default_connection_timeout(),
242 idle_timeout_secs: default_idle_timeout(),
243 max_lifetime_secs: default_max_lifetime(),
244 }
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
254 fn test_default_clickhouse_db_should_return_riglr() {
255 assert_eq!(default_clickhouse_db(), "riglr");
256 }
257
258 #[test]
259 fn test_default_max_connections_should_return_10() {
260 assert_eq!(default_max_connections(), 10);
261 }
262
263 #[test]
264 fn test_default_min_connections_should_return_2() {
265 assert_eq!(default_min_connections(), 2);
266 }
267
268 #[test]
269 fn test_default_connection_timeout_should_return_30() {
270 assert_eq!(default_connection_timeout(), 30);
271 }
272
273 #[test]
274 fn test_default_idle_timeout_should_return_600() {
275 assert_eq!(default_idle_timeout(), 600);
276 }
277
278 #[test]
279 fn test_default_max_lifetime_should_return_3600() {
280 assert_eq!(default_max_lifetime(), 3600);
281 }
282
283 #[test]
285 fn test_pool_config_default_should_use_default_values() {
286 let pool = PoolConfig::default();
287 assert_eq!(pool.max_connections, 10);
288 assert_eq!(pool.min_connections, 2);
289 assert_eq!(pool.connection_timeout_secs, 30);
290 assert_eq!(pool.idle_timeout_secs, 600);
291 assert_eq!(pool.max_lifetime_secs, 3600);
292 }
293
294 #[test]
295 fn test_database_config_default_should_use_default_values() {
296 let config = DatabaseConfig::default();
297 assert_eq!(config.redis_url, "redis://localhost:6379");
298 assert_eq!(config.neo4j_url, None);
299 assert_eq!(config.neo4j_username, None);
300 assert_eq!(config.neo4j_password, None);
301 assert_eq!(config.clickhouse_url, None);
302 assert_eq!(config.clickhouse_database, "riglr");
303 assert_eq!(config.postgres_url, None);
304 assert_eq!(config.pool.max_connections, 10);
305 }
306
307 #[test]
309 fn test_pool_config_validate_when_valid_should_return_ok() {
310 let pool = PoolConfig {
311 max_connections: 10,
312 min_connections: 2,
313 connection_timeout_secs: 30,
314 idle_timeout_secs: 600,
315 max_lifetime_secs: 3600,
316 };
317 assert!(pool.validate_config().is_ok());
318 }
319
320 #[test]
321 fn test_pool_config_validate_when_min_equals_max_should_return_ok() {
322 let pool = PoolConfig {
323 max_connections: 5,
324 min_connections: 5,
325 connection_timeout_secs: 30,
326 idle_timeout_secs: 600,
327 max_lifetime_secs: 3600,
328 };
329 assert!(pool.validate_config().is_ok());
330 }
331
332 #[test]
334 fn test_pool_config_validate_when_max_connections_zero_should_return_err() {
335 let pool = PoolConfig {
336 max_connections: 0,
337 min_connections: 0,
338 connection_timeout_secs: 30,
339 idle_timeout_secs: 600,
340 max_lifetime_secs: 3600,
341 };
342 let result = pool.validate_config();
343 assert!(result.is_err());
344 let err_msg = result.unwrap_err().to_string();
346 assert!(err_msg.contains("Validation failed") || err_msg.contains("max_connections"));
347 }
348
349 #[test]
350 fn test_pool_config_validate_when_min_greater_than_max_should_return_err() {
351 let pool = PoolConfig {
352 max_connections: 5,
353 min_connections: 10,
354 connection_timeout_secs: 30,
355 idle_timeout_secs: 600,
356 max_lifetime_secs: 3600,
357 };
358 let result = pool.validate_config();
359 assert!(result.is_err());
360 assert!(result
361 .unwrap_err()
362 .to_string()
363 .contains("min_connections cannot be greater than max_connections"));
364 }
365
366 #[test]
367 fn test_pool_config_validate_when_connection_timeout_zero_should_return_err() {
368 let pool = PoolConfig {
369 max_connections: 10,
370 min_connections: 2,
371 connection_timeout_secs: 0,
372 idle_timeout_secs: 600,
373 max_lifetime_secs: 3600,
374 };
375 let result = pool.validate_config();
376 assert!(result.is_err());
377 let err_msg = result.unwrap_err().to_string();
379 assert!(
380 err_msg.contains("Validation failed") || err_msg.contains("connection_timeout_secs")
381 );
382 }
383
384 #[test]
386 fn test_validate_neo4j_url_when_neo4j_scheme_should_return_ok() {
387 assert!(validate_neo4j_url("neo4j://localhost:7687").is_ok());
388 }
389
390 #[test]
391 fn test_validate_neo4j_url_when_neo4j_secure_scheme_should_return_ok() {
392 assert!(validate_neo4j_url("neo4j+s://localhost:7687").is_ok());
393 }
394
395 #[test]
396 fn test_validate_neo4j_url_when_bolt_scheme_should_return_ok() {
397 assert!(validate_neo4j_url("bolt://localhost:7687").is_ok());
398 }
399
400 #[test]
401 fn test_validate_neo4j_url_when_bolt_secure_scheme_should_return_ok() {
402 assert!(validate_neo4j_url("bolt+s://localhost:7687").is_ok());
403 }
404
405 #[test]
407 fn test_validate_neo4j_url_when_invalid_scheme_should_return_err() {
408 let result = validate_neo4j_url("http://localhost:7687");
409 assert!(result.is_err());
410 assert!(result
411 .unwrap_err()
412 .to_string()
413 .contains("NEO4J_URL must start with neo4j://"));
414 }
415
416 #[test]
417 fn test_validate_neo4j_url_when_malformed_url_should_return_err() {
418 let result = validate_neo4j_url("neo4j://[invalid");
419 assert!(result.is_err());
420 assert!(result
421 .unwrap_err()
422 .to_string()
423 .contains("Invalid Neo4j URL"));
424 }
425
426 #[test]
428 fn test_validate_clickhouse_url_when_http_scheme_should_return_ok() {
429 assert!(validate_clickhouse_url("http://localhost:8123").is_ok());
430 }
431
432 #[test]
433 fn test_validate_clickhouse_url_when_https_scheme_should_return_ok() {
434 assert!(validate_clickhouse_url("https://localhost:8443").is_ok());
435 }
436
437 #[test]
439 fn test_validate_clickhouse_url_when_invalid_scheme_should_return_err() {
440 let result = validate_clickhouse_url("tcp://localhost:8123");
441 assert!(result.is_err());
442 assert!(result
443 .unwrap_err()
444 .to_string()
445 .contains("CLICKHOUSE_URL must start with http:// or https://"));
446 }
447
448 #[test]
449 fn test_validate_clickhouse_url_when_malformed_url_should_return_err() {
450 let result = validate_clickhouse_url("http://[invalid");
451 assert!(result.is_err());
452 assert!(result
453 .unwrap_err()
454 .to_string()
455 .contains("Invalid ClickHouse URL"));
456 }
457
458 #[test]
460 fn test_validate_postgres_url_when_postgres_scheme_should_return_ok() {
461 assert!(validate_postgres_url("postgres://localhost:5432/db").is_ok());
462 }
463
464 #[test]
465 fn test_validate_postgres_url_when_postgresql_scheme_should_return_ok() {
466 assert!(validate_postgres_url("postgresql://localhost:5432/db").is_ok());
467 }
468
469 #[test]
471 fn test_validate_postgres_url_when_invalid_scheme_should_return_err() {
472 let result = validate_postgres_url("mysql://localhost:5432/db");
473 assert!(result.is_err());
474 assert!(result
475 .unwrap_err()
476 .to_string()
477 .contains("POSTGRES_URL must start with postgres:// or postgresql://"));
478 }
479
480 #[test]
481 fn test_validate_postgres_url_when_malformed_url_should_return_err() {
482 let result = validate_postgres_url("postgres://[invalid");
483 assert!(result.is_err());
484 assert!(result
485 .unwrap_err()
486 .to_string()
487 .contains("Invalid PostgreSQL URL"));
488 }
489
490 #[test]
492 fn test_database_config_validate_when_minimal_config_should_return_ok() {
493 let config = DatabaseConfig {
494 redis_url: "redis://localhost:6379".to_string(),
495 neo4j_url: None,
496 neo4j_username: None,
497 neo4j_password: None,
498 clickhouse_url: None,
499 clickhouse_database: "riglr".to_string(),
500 postgres_url: None,
501 pool: PoolConfig::default(),
502 };
503 assert!(config.validate_config().is_ok());
504 }
505
506 #[test]
507 fn test_database_config_validate_when_all_urls_provided_should_return_ok() {
508 let config = DatabaseConfig {
509 redis_url: "redis://localhost:6379".to_string(),
510 neo4j_url: Some("neo4j://localhost:7687".to_string()),
511 neo4j_username: Some("neo4j".to_string()),
512 neo4j_password: Some("password".to_string()),
513 clickhouse_url: Some("http://localhost:8123".to_string()),
514 clickhouse_database: "riglr".to_string(),
515 postgres_url: Some("postgres://localhost:5432/db".to_string()),
516 pool: PoolConfig::default(),
517 };
518 assert!(config.validate_config().is_ok());
519 }
520
521 #[test]
523 fn test_database_config_validate_when_invalid_redis_url_should_return_err() {
524 let config = DatabaseConfig {
525 redis_url: "http://localhost:6379".to_string(),
526 ..Default::default()
527 };
528 let result = config.validate_config();
529 assert!(result.is_err());
530 assert!(result
531 .unwrap_err()
532 .to_string()
533 .contains("REDIS_URL must start with redis:// or rediss://"));
534 }
535
536 #[test]
537 fn test_database_config_validate_when_invalid_neo4j_url_should_return_err() {
538 let config = DatabaseConfig {
539 redis_url: "redis://localhost:6379".to_string(),
540 neo4j_url: Some("http://localhost:7687".to_string()),
541 ..Default::default()
542 };
543 let result = config.validate_config();
544 assert!(result.is_err());
545 assert!(result
546 .unwrap_err()
547 .to_string()
548 .contains("NEO4J_URL must start with neo4j://"));
549 }
550
551 #[test]
552 fn test_database_config_validate_when_invalid_clickhouse_url_should_return_err() {
553 let config = DatabaseConfig {
554 redis_url: "redis://localhost:6379".to_string(),
555 clickhouse_url: Some("tcp://localhost:8123".to_string()),
556 ..Default::default()
557 };
558 let result = config.validate_config();
559 assert!(result.is_err());
560 assert!(result
561 .unwrap_err()
562 .to_string()
563 .contains("CLICKHOUSE_URL must start with http:// or https://"));
564 }
565
566 #[test]
567 fn test_database_config_validate_when_invalid_postgres_url_should_return_err() {
568 let config = DatabaseConfig {
569 redis_url: "redis://localhost:6379".to_string(),
570 postgres_url: Some("mysql://localhost:5432/db".to_string()),
571 ..Default::default()
572 };
573 let result = config.validate_config();
574 assert!(result.is_err());
575 assert!(result
576 .unwrap_err()
577 .to_string()
578 .contains("POSTGRES_URL must start with postgres:// or postgresql://"));
579 }
580
581 #[test]
582 fn test_database_config_validate_when_invalid_pool_config_should_return_err() {
583 let config = DatabaseConfig {
584 redis_url: "redis://localhost:6379".to_string(),
585 pool: PoolConfig {
586 max_connections: 0,
587 min_connections: 0,
588 connection_timeout_secs: 30,
589 idle_timeout_secs: 600,
590 max_lifetime_secs: 3600,
591 },
592 ..Default::default()
593 };
594 let result = config.validate_config();
595 assert!(result.is_err());
596 let err_msg = result.unwrap_err().to_string();
598 assert!(err_msg.contains("Validation failed") || err_msg.contains("max_connections"));
599 }
600
601 #[test]
604 fn test_validate_neo4j_url_when_neo4j_prefix_in_middle_should_return_err() {
605 let result = validate_neo4j_url("http://neo4j://localhost:7687");
606 assert!(result.is_err());
607 assert!(result
608 .unwrap_err()
609 .to_string()
610 .contains("NEO4J_URL must start with neo4j://"));
611 }
612
613 #[test]
614 fn test_validate_clickhouse_url_when_http_prefix_in_middle_should_return_err() {
615 let result = validate_clickhouse_url("tcp://http://localhost:8123");
616 assert!(result.is_err());
617 assert!(result
618 .unwrap_err()
619 .to_string()
620 .contains("CLICKHOUSE_URL must start with http:// or https://"));
621 }
622
623 #[test]
624 fn test_validate_postgres_url_when_postgres_prefix_in_middle_should_return_err() {
625 let result = validate_postgres_url("mysql://postgres://localhost:5432/db");
626 assert!(result.is_err());
627 assert!(result
628 .unwrap_err()
629 .to_string()
630 .contains("POSTGRES_URL must start with postgres:// or postgresql://"));
631 }
632
633 #[test]
635 fn test_pool_config_validate_when_very_large_values_should_return_ok() {
636 let pool = PoolConfig {
637 max_connections: 1000, min_connections: 100, connection_timeout_secs: 300, idle_timeout_secs: u64::MAX, max_lifetime_secs: u64::MAX, };
643 assert!(pool.validate_config().is_ok());
644 }
645
646 #[test]
647 fn test_pool_config_validate_when_min_zero_max_one_should_return_ok() {
648 let pool = PoolConfig {
649 max_connections: 1,
650 min_connections: 0,
651 connection_timeout_secs: 1,
652 idle_timeout_secs: 0,
653 max_lifetime_secs: 0,
654 };
655 assert!(pool.validate_config().is_ok());
656 }
657}