1use serde::{Deserialize, Serialize};
2use std::time::Duration;
3
4#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(tag = "type", rename_all = "lowercase")]
7pub enum StorageConfig {
8 Memory(MemoryConfig),
10 #[cfg(feature = "redis")]
12 Redis(RedisConfig),
13 #[cfg(feature = "postgres")]
15 Postgres(PostgresConfig),
16}
17
18impl Default for StorageConfig {
19 fn default() -> Self {
20 Self::Memory(MemoryConfig::default())
21 }
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26pub struct MemoryConfig {
27 pub max_jobs: Option<usize>,
29 pub auto_cleanup: bool,
31 pub cleanup_interval: Option<Duration>,
33}
34
35impl Default for MemoryConfig {
36 fn default() -> Self {
37 Self {
38 max_jobs: Some(10_000),
39 auto_cleanup: true,
40 cleanup_interval: Some(Duration::from_secs(300)), }
42 }
43}
44
45impl MemoryConfig {
46 pub fn new() -> Self {
48 Self::default()
49 }
50
51 pub fn with_max_jobs(mut self, max_jobs: usize) -> Self {
53 self.max_jobs = Some(max_jobs);
54 self
55 }
56
57 pub fn unlimited(mut self) -> Self {
59 self.max_jobs = None;
60 self
61 }
62
63 pub fn with_auto_cleanup(mut self, enabled: bool) -> Self {
65 self.auto_cleanup = enabled;
66 self
67 }
68
69 pub fn with_cleanup_interval(mut self, interval: Duration) -> Self {
71 self.cleanup_interval = Some(interval);
72 self
73 }
74}
75
76#[cfg(feature = "redis")]
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79pub struct RedisConfig {
80 pub url: String,
82 pub pool_size: u32,
84 pub connection_timeout: Duration,
86 pub command_timeout: Duration,
88 pub key_prefix: String,
90 pub database: Option<u8>,
92 pub username: Option<String>,
94 pub password: Option<String>,
96 pub tls: bool,
98 pub completed_job_ttl: Option<Duration>,
100 pub failed_job_ttl: Option<Duration>,
102}
103
104#[cfg(feature = "redis")]
105impl Default for RedisConfig {
106 fn default() -> Self {
107 Self {
108 url: std::env::var("REDIS_URL").expect("REDIS_URL environment variable must be set"),
109 pool_size: 10,
110 connection_timeout: Duration::from_secs(5),
111 command_timeout: Duration::from_secs(5),
112 key_prefix: "qml".to_string(),
113 database: None,
114 username: std::env::var("REDIS_USERNAME")
115 .ok()
116 .filter(|s| !s.is_empty()),
117 password: std::env::var("REDIS_PASSWORD")
118 .ok()
119 .filter(|s| !s.is_empty()),
120 tls: false,
121 completed_job_ttl: None,
122 failed_job_ttl: None,
123 }
124 }
125}
126
127#[cfg(feature = "redis")]
128impl RedisConfig {
129 pub fn new() -> Self {
131 Self::default()
132 }
133 pub fn with_url<S: Into<String>>(mut self, url: S) -> Self {
135 self.url = url.into();
136 self
137 }
138
139 pub fn with_pool_size(mut self, size: u32) -> Self {
141 self.pool_size = size;
142 self
143 }
144
145 pub fn with_connection_timeout(mut self, timeout: Duration) -> Self {
147 self.connection_timeout = timeout;
148 self
149 }
150
151 pub fn with_command_timeout(mut self, timeout: Duration) -> Self {
153 self.command_timeout = timeout;
154 self
155 }
156
157 pub fn with_key_prefix<S: Into<String>>(mut self, prefix: S) -> Self {
159 self.key_prefix = prefix.into();
160 self
161 }
162
163 pub fn with_database(mut self, database: u8) -> Self {
165 self.database = Some(database);
166 self
167 }
168
169 pub fn with_credentials<U: Into<String>, P: Into<String>>(
171 mut self,
172 username: U,
173 password: P,
174 ) -> Self {
175 self.username = Some(username.into());
176 self.password = Some(password.into());
177 self
178 }
179
180 pub fn with_password<P: Into<String>>(mut self, password: P) -> Self {
182 self.password = Some(password.into());
183 self
184 }
185
186 pub fn with_tls(mut self, enabled: bool) -> Self {
188 self.tls = enabled;
189 self
190 }
191
192 pub fn with_completed_job_ttl(mut self, ttl: Duration) -> Self {
194 self.completed_job_ttl = Some(ttl);
195 self
196 }
197
198 pub fn with_failed_job_ttl(mut self, ttl: Duration) -> Self {
200 self.failed_job_ttl = Some(ttl);
201 self
202 }
203
204 pub fn no_completed_job_ttl(mut self) -> Self {
206 self.completed_job_ttl = None;
207 self
208 }
209
210 pub fn no_failed_job_ttl(mut self) -> Self {
212 self.failed_job_ttl = None;
213 self
214 }
215
216 pub fn full_url(&self) -> String {
218 let mut url = self.url.clone();
219
220 if let (Some(username), Some(password)) = (&self.username, &self.password) {
222 url = url.replace("redis://", &format!("redis://{}:{}@", username, password));
223 } else if let Some(password) = &self.password {
224 url = url.replace("redis://", &format!("redis://:{}@", password));
225 }
226
227 if let Some(db) = self.database {
229 if !url.ends_with('/') {
230 url.push('/');
231 }
232 url.push_str(&db.to_string());
233 }
234
235 url
236 }
237}
238
239#[cfg(feature = "postgres")]
241#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
242pub struct PostgresConfig {
243 pub database_url: String,
245 pub max_connections: u32,
247 pub min_connections: u32,
249 pub connect_timeout: Duration,
251 pub command_timeout: Duration,
253 pub table_name: String,
255 pub schema_name: String,
257 pub auto_migrate: bool,
259 pub idle_timeout: Duration,
261 pub max_lifetime: Option<Duration>,
263 pub require_ssl: bool,
265}
266
267#[cfg(feature = "postgres")]
268impl Default for PostgresConfig {
269 fn default() -> Self {
270 Self {
271 database_url: std::env::var("DATABASE_URL")
272 .expect("DATABASE_URL environment variable must be set"),
273 migrations_path: "./migrations".to_string(),
274 max_connections: 20,
275 min_connections: 1,
276 connect_timeout: Duration::from_secs(30),
277 command_timeout: Duration::from_secs(30),
278 table_name: "qml_jobs".to_string(),
279 schema_name: "public".to_string(),
280 auto_migrate: true,
281 idle_timeout: Duration::from_secs(600),
282 max_lifetime: Some(Duration::from_secs(1800)),
283 require_ssl: false,
284 }
285 }
286}
287
288#[cfg(feature = "postgres")]
289impl PostgresConfig {
290 pub fn new() -> Self {
292 Self::default()
293 }
294 pub fn with_database_url<S: Into<String>>(mut self, url: S) -> Self {
296 self.database_url = url.into();
297 self
298 }
299
300 pub fn with_migrations_path<S: Into<String>>(mut self, path: S) -> Self {
302 self.migrations_path = path.into();
303 self
304 }
305
306 pub fn with_max_connections(mut self, max: u32) -> Self {
308 self.max_connections = max;
309 self
310 }
311
312 pub fn with_min_connections(mut self, min: u32) -> Self {
314 self.min_connections = min;
315 self
316 }
317
318 pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
320 self.connect_timeout = timeout;
321 self
322 }
323
324 pub fn with_command_timeout(mut self, timeout: Duration) -> Self {
326 self.command_timeout = timeout;
327 self
328 }
329
330 pub fn with_table_name<S: Into<String>>(mut self, name: S) -> Self {
332 self.table_name = name.into();
333 self
334 }
335
336 pub fn with_schema_name<S: Into<String>>(mut self, name: S) -> Self {
338 self.schema_name = name.into();
339 self
340 }
341
342 pub fn with_auto_migrate(mut self, enabled: bool) -> Self {
344 self.auto_migrate = enabled;
345 self
346 }
347
348 pub fn with_idle_timeout(mut self, timeout: Duration) -> Self {
350 self.idle_timeout = timeout;
351 self
352 }
353
354 pub fn with_max_lifetime(mut self, lifetime: Duration) -> Self {
356 self.max_lifetime = Some(lifetime);
357 self
358 }
359
360 pub fn without_max_lifetime(mut self) -> Self {
362 self.max_lifetime = None;
363 self
364 }
365
366 pub fn with_ssl(mut self, require_ssl: bool) -> Self {
368 self.require_ssl = require_ssl;
369 self
370 }
371
372 pub fn full_table_name(&self) -> String {
374 format!("{}.{}", self.schema_name, self.table_name)
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 #[test]
383 fn test_memory_config_default() {
384 let config = MemoryConfig::default();
385 assert_eq!(config.max_jobs, Some(10_000));
386 assert!(config.auto_cleanup);
387 assert_eq!(config.cleanup_interval, Some(Duration::from_secs(300)));
388 }
389
390 #[test]
391 fn test_memory_config_builder() {
392 let config = MemoryConfig::new()
393 .with_max_jobs(5_000)
394 .with_auto_cleanup(false)
395 .with_cleanup_interval(Duration::from_secs(600));
396
397 assert_eq!(config.max_jobs, Some(5_000));
398 assert!(!config.auto_cleanup);
399 assert_eq!(config.cleanup_interval, Some(Duration::from_secs(600)));
400 }
401
402 #[test]
403 #[cfg(feature = "redis")]
404 fn test_redis_config_default() {
405 std::env::set_var("REDIS_URL", "redis://127.0.0.1:6379");
407
408 let config = RedisConfig::default();
409 assert_eq!(config.url, "redis://127.0.0.1:6379");
410 assert_eq!(config.pool_size, 10);
411 assert_eq!(config.key_prefix, "qml");
412 assert!(!config.tls);
413
414 std::env::remove_var("REDIS_URL");
416 }
417
418 #[test]
419 #[cfg(feature = "redis")]
420 fn test_redis_config_builder() {
421 std::env::set_var("REDIS_URL", "redis://127.0.0.1:6379");
423
424 let config = RedisConfig::new()
425 .with_url("redis://localhost:6380")
426 .with_pool_size(20)
427 .with_key_prefix("test")
428 .with_database(1)
429 .with_credentials("user", "pass")
430 .with_tls(true);
431
432 assert_eq!(config.url, "redis://localhost:6380");
433 assert_eq!(config.pool_size, 20);
434 assert_eq!(config.key_prefix, "test");
435 assert_eq!(config.database, Some(1));
436 assert_eq!(config.username, Some("user".to_string()));
437 assert_eq!(config.password, Some("pass".to_string()));
438 assert!(config.tls);
439
440 std::env::remove_var("REDIS_URL");
442 }
443
444 #[test]
445 #[cfg(feature = "redis")]
446 fn test_redis_full_url() {
447 std::env::set_var("REDIS_URL", "redis://127.0.0.1:6379");
449
450 let config = RedisConfig::new()
451 .with_url("redis://localhost:6379")
452 .with_credentials("user", "pass")
453 .with_database(5);
454
455 assert_eq!(config.full_url(), "redis://user:pass@localhost:6379/5");
456
457 std::env::remove_var("REDIS_URL");
459 }
460
461 #[test]
462 #[cfg(feature = "redis")]
463 fn test_redis_full_url_password_only() {
464 std::env::set_var("REDIS_URL", "redis://127.0.0.1:6379");
466
467 let config = RedisConfig::new()
468 .with_url("redis://localhost:6379")
469 .with_password("pass")
470 .with_database(2);
471
472 assert_eq!(config.full_url(), "redis://:pass@localhost:6379/2");
473
474 std::env::remove_var("REDIS_URL");
476 }
477
478 #[test]
479 #[cfg(feature = "redis")]
480 fn test_storage_config_serialization() {
481 std::env::set_var("REDIS_URL", "redis://127.0.0.1:6379");
483
484 let memory_config = StorageConfig::Memory(MemoryConfig::default());
485 let redis_config = StorageConfig::Redis(RedisConfig::default());
486
487 let memory_json = serde_json::to_string(&memory_config).unwrap();
489 let redis_json = serde_json::to_string(&redis_config).unwrap();
490
491 let _: StorageConfig = serde_json::from_str(&memory_json).unwrap();
492 let _: StorageConfig = serde_json::from_str(&redis_json).unwrap();
493
494 std::env::remove_var("REDIS_URL");
496 }
497
498 #[test]
499 #[cfg(feature = "postgres")]
500 fn test_postgres_config_default() {
501 std::env::set_var(
503 "DATABASE_URL",
504 "postgresql://postgres:password@localhost:5432/qml",
505 );
506
507 let config = PostgresConfig::default();
508 assert_eq!(
509 config.database_url,
510 "postgresql://postgres:password@localhost:5432/qml"
511 );
512 assert_eq!(config.max_connections, 20);
513 assert_eq!(config.min_connections, 1);
514 assert_eq!(config.table_name, "qml_jobs");
515 assert_eq!(config.schema_name, "public");
516 assert!(config.auto_migrate);
517 assert!(!config.require_ssl);
518
519 std::env::remove_var("DATABASE_URL");
521 }
522
523 #[test]
524 #[cfg(feature = "postgres")]
525 fn test_postgres_config_builder() {
526 std::env::set_var(
528 "DATABASE_URL",
529 "postgresql://postgres:password@localhost:5432/qml",
530 );
531
532 let config = PostgresConfig::new()
533 .with_database_url("postgresql://user:pass@localhost:5433/testdb")
534 .with_max_connections(50)
535 .with_min_connections(5)
536 .with_table_name("custom_jobs")
537 .with_schema_name("qml")
538 .with_auto_migrate(false)
539 .with_ssl(true);
540
541 assert_eq!(
542 config.database_url,
543 "postgresql://user:pass@localhost:5433/testdb"
544 );
545 assert_eq!(config.max_connections, 50);
546 assert_eq!(config.min_connections, 5);
547 assert_eq!(config.table_name, "custom_jobs");
548 assert_eq!(config.schema_name, "qml");
549 assert!(!config.auto_migrate);
550 assert!(config.require_ssl);
551
552 std::env::remove_var("DATABASE_URL");
554 }
555
556 #[test]
557 #[cfg(feature = "postgres")]
558 fn test_postgres_full_table_name() {
559 std::env::set_var(
561 "DATABASE_URL",
562 "postgresql://postgres:password@localhost:5432/qml",
563 );
564
565 let config = PostgresConfig::new()
566 .with_schema_name("qml")
567 .with_table_name("jobs");
568
569 assert_eq!(config.full_table_name(), "qml.jobs");
570
571 std::env::remove_var("DATABASE_URL");
573 }
574
575 #[test]
576 #[cfg(feature = "postgres")]
577 fn test_postgres_config_serialization() {
578 std::env::set_var(
580 "DATABASE_URL",
581 "postgresql://postgres:password@localhost:5432/qml",
582 );
583
584 let postgres_config = StorageConfig::Postgres(PostgresConfig::default());
585
586 let postgres_json = serde_json::to_string(&postgres_config).unwrap();
588 let _: StorageConfig = serde_json::from_str(&postgres_json).unwrap();
589
590 std::env::remove_var("DATABASE_URL");
592 }
593}