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")
109 .unwrap_or_else(|_| "redis://localhost:6379".to_string()),
110 pool_size: 10,
111 connection_timeout: Duration::from_secs(5),
112 command_timeout: Duration::from_secs(5),
113 key_prefix: "qml".to_string(),
114 database: None,
115 username: std::env::var("REDIS_USERNAME")
116 .ok()
117 .filter(|s| !s.is_empty()),
118 password: std::env::var("REDIS_PASSWORD")
119 .ok()
120 .filter(|s| !s.is_empty()),
121 tls: false,
122 completed_job_ttl: None,
123 failed_job_ttl: None,
124 }
125 }
126}
127
128#[cfg(feature = "redis")]
129impl RedisConfig {
130 pub fn new() -> Self {
132 Self::default()
133 }
134 pub fn with_url<S: Into<String>>(mut self, url: S) -> Self {
136 self.url = url.into();
137 self
138 }
139
140 pub fn with_pool_size(mut self, size: u32) -> Self {
142 self.pool_size = size;
143 self
144 }
145
146 pub fn with_connection_timeout(mut self, timeout: Duration) -> Self {
148 self.connection_timeout = timeout;
149 self
150 }
151
152 pub fn with_command_timeout(mut self, timeout: Duration) -> Self {
154 self.command_timeout = timeout;
155 self
156 }
157
158 pub fn with_key_prefix<S: Into<String>>(mut self, prefix: S) -> Self {
160 self.key_prefix = prefix.into();
161 self
162 }
163
164 pub fn with_database(mut self, database: u8) -> Self {
166 self.database = Some(database);
167 self
168 }
169
170 pub fn with_credentials<U: Into<String>, P: Into<String>>(
172 mut self,
173 username: U,
174 password: P,
175 ) -> Self {
176 self.username = Some(username.into());
177 self.password = Some(password.into());
178 self
179 }
180
181 pub fn with_password<P: Into<String>>(mut self, password: P) -> Self {
183 self.password = Some(password.into());
184 self
185 }
186
187 pub fn with_tls(mut self, enabled: bool) -> Self {
189 self.tls = enabled;
190 self
191 }
192
193 pub fn with_completed_job_ttl(mut self, ttl: Duration) -> Self {
195 self.completed_job_ttl = Some(ttl);
196 self
197 }
198
199 pub fn with_failed_job_ttl(mut self, ttl: Duration) -> Self {
201 self.failed_job_ttl = Some(ttl);
202 self
203 }
204
205 pub fn no_completed_job_ttl(mut self) -> Self {
207 self.completed_job_ttl = None;
208 self
209 }
210
211 pub fn no_failed_job_ttl(mut self) -> Self {
213 self.failed_job_ttl = None;
214 self
215 }
216
217 pub fn full_url(&self) -> String {
219 let mut url = self.url.clone();
220
221 if let (Some(username), Some(password)) = (&self.username, &self.password) {
223 url = url.replace("redis://", &format!("redis://{}:{}@", username, password));
224 } else if let Some(password) = &self.password {
225 url = url.replace("redis://", &format!("redis://:{}@", password));
226 }
227
228 if let Some(db) = self.database {
230 if !url.ends_with('/') {
231 url.push('/');
232 }
233 url.push_str(&db.to_string());
234 }
235
236 url
237 }
238}
239
240#[cfg(feature = "postgres")]
242#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
243pub struct PostgresConfig {
244 pub database_url: String,
246 pub max_connections: u32,
248 pub min_connections: u32,
250 pub connect_timeout: Duration,
252 pub command_timeout: Duration,
254 pub table_name: String,
256 pub schema_name: String,
258 pub auto_migrate: bool,
260 pub idle_timeout: Duration,
262 pub max_lifetime: Option<Duration>,
264 pub require_ssl: bool,
266}
267
268#[cfg(feature = "postgres")]
269impl Default for PostgresConfig {
270 fn default() -> Self {
271 Self {
272 database_url: std::env::var("DATABASE_URL").unwrap_or_else(|_| {
273 "postgresql://postgres:password@localhost:5432/qml".to_string()
274 }),
275 max_connections: 20,
276 min_connections: 1,
277 connect_timeout: Duration::from_secs(30),
278 command_timeout: Duration::from_secs(30),
279 table_name: "qml_jobs".to_string(),
280 schema_name: "qml".to_string(),
281 auto_migrate: true,
282 idle_timeout: Duration::from_secs(600),
283 max_lifetime: Some(Duration::from_secs(1800)),
284 require_ssl: false,
285 }
286 }
287}
288
289#[cfg(feature = "postgres")]
290impl PostgresConfig {
291 pub fn new() -> Self {
293 Self::default()
294 }
295
296 pub fn with_defaults() -> Self {
298 Self {
299 database_url: String::new(), max_connections: 20,
301 min_connections: 1,
302 connect_timeout: Duration::from_secs(30),
303 command_timeout: Duration::from_secs(30),
304 table_name: "qml_jobs".to_string(),
305 schema_name: "qml".to_string(),
306 auto_migrate: true,
307 idle_timeout: Duration::from_secs(600),
308 max_lifetime: Some(Duration::from_secs(1800)),
309 require_ssl: false,
310 }
311 }
312
313 pub fn with_database_url<S: Into<String>>(mut self, url: S) -> Self {
315 self.database_url = url.into();
316 self
317 }
318
319 pub fn with_max_connections(mut self, max: u32) -> Self {
321 self.max_connections = max;
322 self
323 }
324
325 pub fn with_min_connections(mut self, min: u32) -> Self {
327 self.min_connections = min;
328 self
329 }
330
331 pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
333 self.connect_timeout = timeout;
334 self
335 }
336
337 pub fn with_command_timeout(mut self, timeout: Duration) -> Self {
339 self.command_timeout = timeout;
340 self
341 }
342
343 pub fn with_table_name<S: Into<String>>(mut self, name: S) -> Self {
345 self.table_name = name.into();
346 self
347 }
348
349 pub fn with_schema_name<S: Into<String>>(mut self, name: S) -> Self {
351 self.schema_name = name.into();
352 self
353 }
354
355 pub fn with_auto_migrate(mut self, enabled: bool) -> Self {
357 self.auto_migrate = enabled;
358 self
359 }
360
361 pub fn with_idle_timeout(mut self, timeout: Duration) -> Self {
363 self.idle_timeout = timeout;
364 self
365 }
366
367 pub fn with_max_lifetime(mut self, lifetime: Duration) -> Self {
369 self.max_lifetime = Some(lifetime);
370 self
371 }
372
373 pub fn without_max_lifetime(mut self) -> Self {
375 self.max_lifetime = None;
376 self
377 }
378
379 pub fn with_ssl(mut self, require_ssl: bool) -> Self {
381 self.require_ssl = require_ssl;
382 self
383 }
384
385 pub fn full_table_name(&self) -> String {
387 format!("{}.{}", self.schema_name, self.table_name)
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394
395 #[test]
396 fn test_memory_config_default() {
397 let config = MemoryConfig::default();
398 assert_eq!(config.max_jobs, Some(10_000));
399 assert!(config.auto_cleanup);
400 assert_eq!(config.cleanup_interval, Some(Duration::from_secs(300)));
401 }
402
403 #[test]
404 fn test_memory_config_builder() {
405 let config = MemoryConfig::new()
406 .with_max_jobs(5_000)
407 .with_auto_cleanup(false)
408 .with_cleanup_interval(Duration::from_secs(600));
409
410 assert_eq!(config.max_jobs, Some(5_000));
411 assert!(!config.auto_cleanup);
412 assert_eq!(config.cleanup_interval, Some(Duration::from_secs(600)));
413 }
414
415 #[test]
416 #[cfg(feature = "redis")]
417 fn test_redis_config_default() {
418 let config = RedisConfig::default();
419 assert_eq!(config.url, "redis://localhost:6379");
421 assert_eq!(config.pool_size, 10);
422 assert_eq!(config.key_prefix, "qml");
423 assert!(!config.tls);
424 }
425
426 #[test]
427 #[cfg(feature = "redis")]
428 fn test_redis_config_builder() {
429 let config = RedisConfig::new()
430 .with_url("redis://localhost:6380")
431 .with_pool_size(20)
432 .with_key_prefix("test")
433 .with_database(1)
434 .with_credentials("user", "pass")
435 .with_tls(true);
436
437 assert_eq!(config.url, "redis://localhost:6380");
438 assert_eq!(config.pool_size, 20);
439 assert_eq!(config.key_prefix, "test");
440 assert_eq!(config.database, Some(1));
441 assert_eq!(config.username, Some("user".to_string()));
442 assert_eq!(config.password, Some("pass".to_string()));
443 assert!(config.tls);
444 }
445
446 #[test]
447 #[cfg(feature = "redis")]
448 fn test_redis_full_url() {
449 let config = RedisConfig::new()
450 .with_url("redis://localhost:6379")
451 .with_credentials("user", "pass")
452 .with_database(5);
453
454 assert_eq!(config.full_url(), "redis://user:pass@localhost:6379/5");
455 }
456
457 #[test]
458 #[cfg(feature = "redis")]
459 fn test_redis_full_url_password_only() {
460 let config = RedisConfig::new()
461 .with_url("redis://localhost:6379")
462 .with_password("pass")
463 .with_database(2);
464
465 assert_eq!(config.full_url(), "redis://:pass@localhost:6379/2");
466 }
467
468 #[test]
469 #[cfg(feature = "redis")]
470 fn test_storage_config_serialization() {
471 let memory_config = StorageConfig::Memory(MemoryConfig::default());
472 let redis_config = StorageConfig::Redis(RedisConfig::default());
473
474 let memory_json = serde_json::to_string(&memory_config).unwrap();
476 let redis_json = serde_json::to_string(&redis_config).unwrap();
477
478 let _: StorageConfig = serde_json::from_str(&memory_json).unwrap();
479 let _: StorageConfig = serde_json::from_str(&redis_json).unwrap();
480 }
481
482 #[test]
483 #[cfg(feature = "postgres")]
484 fn test_postgres_config_default() {
485 let config = PostgresConfig::default();
486 assert_eq!(
487 config.database_url,
488 "postgresql://postgres:password@localhost:5432/qml"
489 );
490 assert_eq!(config.max_connections, 20);
491 assert_eq!(config.min_connections, 1);
492 assert_eq!(config.table_name, "qml_jobs");
493 assert_eq!(config.schema_name, "qml");
494 assert!(config.auto_migrate);
495 assert!(!config.require_ssl);
496 }
497
498 #[test]
499 #[cfg(feature = "postgres")]
500 fn test_postgres_config_builder() {
501 let config = PostgresConfig::new()
502 .with_database_url("postgresql://user:pass@localhost:5433/testdb")
503 .with_max_connections(50)
504 .with_min_connections(5)
505 .with_table_name("custom_jobs")
506 .with_schema_name("qml")
507 .with_auto_migrate(false)
508 .with_ssl(true);
509
510 assert_eq!(
511 config.database_url,
512 "postgresql://user:pass@localhost:5433/testdb"
513 );
514 assert_eq!(config.max_connections, 50);
515 assert_eq!(config.min_connections, 5);
516 assert_eq!(config.table_name, "custom_jobs");
517 assert_eq!(config.schema_name, "qml");
518 assert!(!config.auto_migrate);
519 assert!(config.require_ssl);
520 }
521
522 #[test]
523 #[cfg(feature = "postgres")]
524 fn test_postgres_full_table_name() {
525 let config = PostgresConfig::new()
526 .with_schema_name("qml")
527 .with_table_name("jobs");
528
529 assert_eq!(config.full_table_name(), "qml.jobs");
530 }
531
532 #[test]
533 #[cfg(feature = "postgres")]
534 fn test_postgres_config_serialization() {
535 let postgres_config = StorageConfig::Postgres(PostgresConfig::default());
536
537 let postgres_json = serde_json::to_string(&postgres_config).unwrap();
539 let _: StorageConfig = serde_json::from_str(&postgres_json).unwrap();
540 }
541}