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)]
34pub struct MemoryConfig {
35 pub max_jobs: Option<usize>,
37 #[deprecated(
43 note = "auto_cleanup is a no-op; CleanupWorker handles expiration. Will be removed."
44 )]
45 pub auto_cleanup: bool,
46 #[deprecated(
49 note = "cleanup_interval is a no-op; configure CleanupWorker via ServerConfig instead."
50 )]
51 pub cleanup_interval: Option<Duration>,
52}
53
54impl Default for MemoryConfig {
55 fn default() -> Self {
56 #[allow(deprecated)]
60 Self {
61 max_jobs: Some(10_000),
62 auto_cleanup: true,
63 cleanup_interval: Some(Duration::from_secs(300)),
64 }
65 }
66}
67
68impl MemoryConfig {
69 pub fn new() -> Self {
71 Self::default()
72 }
73
74 pub fn with_max_jobs(mut self, max_jobs: usize) -> Self {
76 self.max_jobs = Some(max_jobs);
77 self
78 }
79
80 pub fn unlimited(mut self) -> Self {
82 self.max_jobs = None;
83 self
84 }
85
86 #[deprecated(
89 note = "auto_cleanup is a no-op; CleanupWorker handles expiration. Will be removed."
90 )]
91 pub fn with_auto_cleanup(mut self, enabled: bool) -> Self {
92 #[allow(deprecated)]
93 {
94 self.auto_cleanup = enabled;
95 }
96 self
97 }
98
99 #[deprecated(
102 note = "cleanup_interval is a no-op; configure CleanupWorker via ServerConfig instead."
103 )]
104 pub fn with_cleanup_interval(mut self, interval: Duration) -> Self {
105 #[allow(deprecated)]
106 {
107 self.cleanup_interval = Some(interval);
108 }
109 self
110 }
111}
112
113#[cfg(feature = "redis")]
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116pub struct RedisConfig {
117 pub url: String,
119 pub pool_size: u32,
121 pub connection_timeout: Duration,
123 pub command_timeout: Duration,
125 pub key_prefix: String,
127 pub database: Option<u8>,
129 pub username: Option<String>,
131 pub password: Option<String>,
133 pub tls: bool,
135 pub completed_job_ttl: Option<Duration>,
137 pub failed_job_ttl: Option<Duration>,
139}
140
141#[cfg(feature = "redis")]
142impl Default for RedisConfig {
143 fn default() -> Self {
144 Self {
145 url: std::env::var("REDIS_URL")
146 .unwrap_or_else(|_| "redis://localhost:6379".to_string()),
147 pool_size: 10,
148 connection_timeout: Duration::from_secs(5),
149 command_timeout: Duration::from_secs(5),
150 key_prefix: "qml".to_string(),
151 database: None,
152 username: std::env::var("REDIS_USERNAME")
153 .ok()
154 .filter(|s| !s.is_empty()),
155 password: std::env::var("REDIS_PASSWORD")
156 .ok()
157 .filter(|s| !s.is_empty()),
158 tls: false,
159 completed_job_ttl: None,
160 failed_job_ttl: None,
161 }
162 }
163}
164
165#[cfg(feature = "redis")]
166impl RedisConfig {
167 pub fn new() -> Self {
169 Self::default()
170 }
171 pub fn with_url<S: Into<String>>(mut self, url: S) -> Self {
173 self.url = url.into();
174 self
175 }
176
177 pub fn with_pool_size(mut self, size: u32) -> Self {
179 self.pool_size = size;
180 self
181 }
182
183 pub fn with_connection_timeout(mut self, timeout: Duration) -> Self {
185 self.connection_timeout = timeout;
186 self
187 }
188
189 pub fn with_command_timeout(mut self, timeout: Duration) -> Self {
191 self.command_timeout = timeout;
192 self
193 }
194
195 pub fn with_key_prefix<S: Into<String>>(mut self, prefix: S) -> Self {
197 self.key_prefix = prefix.into();
198 self
199 }
200
201 pub fn with_database(mut self, database: u8) -> Self {
203 self.database = Some(database);
204 self
205 }
206
207 pub fn with_credentials<U: Into<String>, P: Into<String>>(
209 mut self,
210 username: U,
211 password: P,
212 ) -> Self {
213 self.username = Some(username.into());
214 self.password = Some(password.into());
215 self
216 }
217
218 pub fn with_password<P: Into<String>>(mut self, password: P) -> Self {
220 self.password = Some(password.into());
221 self
222 }
223
224 pub fn with_tls(mut self, enabled: bool) -> Self {
226 self.tls = enabled;
227 self
228 }
229
230 pub fn with_completed_job_ttl(mut self, ttl: Duration) -> Self {
232 self.completed_job_ttl = Some(ttl);
233 self
234 }
235
236 pub fn with_failed_job_ttl(mut self, ttl: Duration) -> Self {
238 self.failed_job_ttl = Some(ttl);
239 self
240 }
241
242 pub fn no_completed_job_ttl(mut self) -> Self {
244 self.completed_job_ttl = None;
245 self
246 }
247
248 pub fn no_failed_job_ttl(mut self) -> Self {
250 self.failed_job_ttl = None;
251 self
252 }
253
254 pub fn full_url(&self) -> String {
256 let mut url = self.url.clone();
257
258 if let (Some(username), Some(password)) = (&self.username, &self.password) {
260 url = url.replace("redis://", &format!("redis://{}:{}@", username, password));
261 } else if let Some(password) = &self.password {
262 url = url.replace("redis://", &format!("redis://:{}@", password));
263 }
264
265 if let Some(db) = self.database {
267 if !url.ends_with('/') {
268 url.push('/');
269 }
270 url.push_str(&db.to_string());
271 }
272
273 url
274 }
275}
276
277#[cfg(feature = "postgres")]
279#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
280pub struct PostgresConfig {
281 pub database_url: String,
283 pub max_connections: u32,
285 pub min_connections: u32,
287 pub connect_timeout: Duration,
289 pub command_timeout: Duration,
291 pub table_name: String,
293 pub schema_name: String,
295 pub auto_migrate: bool,
297 pub idle_timeout: Duration,
299 pub max_lifetime: Option<Duration>,
301 pub require_ssl: bool,
303}
304
305#[cfg(feature = "postgres")]
306impl Default for PostgresConfig {
307 fn default() -> Self {
308 Self {
309 database_url: std::env::var("DATABASE_URL").unwrap_or_else(|_| {
310 "postgresql://postgres:password@localhost:5432/qml".to_string()
311 }),
312 max_connections: 20,
313 min_connections: 1,
314 connect_timeout: Duration::from_secs(30),
315 command_timeout: Duration::from_secs(30),
316 table_name: "qml_jobs".to_string(),
317 schema_name: "qml".to_string(),
318 auto_migrate: true,
319 idle_timeout: Duration::from_secs(600),
320 max_lifetime: Some(Duration::from_secs(1800)),
321 require_ssl: false,
322 }
323 }
324}
325
326#[cfg(feature = "postgres")]
327impl PostgresConfig {
328 pub fn new() -> Self {
330 Self::default()
331 }
332
333 pub fn with_defaults() -> Self {
335 Self {
336 database_url: String::new(), max_connections: 20,
338 min_connections: 1,
339 connect_timeout: Duration::from_secs(30),
340 command_timeout: Duration::from_secs(30),
341 table_name: "qml_jobs".to_string(),
342 schema_name: "qml".to_string(),
343 auto_migrate: true,
344 idle_timeout: Duration::from_secs(600),
345 max_lifetime: Some(Duration::from_secs(1800)),
346 require_ssl: false,
347 }
348 }
349
350 pub fn with_database_url<S: Into<String>>(mut self, url: S) -> Self {
352 self.database_url = url.into();
353 self
354 }
355
356 pub fn with_max_connections(mut self, max: u32) -> Self {
358 self.max_connections = max;
359 self
360 }
361
362 pub fn with_min_connections(mut self, min: u32) -> Self {
364 self.min_connections = min;
365 self
366 }
367
368 pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
370 self.connect_timeout = timeout;
371 self
372 }
373
374 pub fn with_command_timeout(mut self, timeout: Duration) -> Self {
376 self.command_timeout = timeout;
377 self
378 }
379
380 pub fn with_table_name<S: Into<String>>(mut self, name: S) -> Self {
382 self.table_name = name.into();
383 self
384 }
385
386 pub fn with_schema_name<S: Into<String>>(mut self, name: S) -> Self {
388 self.schema_name = name.into();
389 self
390 }
391
392 pub fn with_auto_migrate(mut self, enabled: bool) -> Self {
394 self.auto_migrate = enabled;
395 self
396 }
397
398 pub fn with_idle_timeout(mut self, timeout: Duration) -> Self {
400 self.idle_timeout = timeout;
401 self
402 }
403
404 pub fn with_max_lifetime(mut self, lifetime: Duration) -> Self {
406 self.max_lifetime = Some(lifetime);
407 self
408 }
409
410 pub fn without_max_lifetime(mut self) -> Self {
412 self.max_lifetime = None;
413 self
414 }
415
416 pub fn with_ssl(mut self, require_ssl: bool) -> Self {
418 self.require_ssl = require_ssl;
419 self
420 }
421
422 pub fn full_table_name(&self) -> String {
424 format!("{}.{}", self.schema_name, self.table_name)
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 #[test]
433 #[allow(deprecated)]
434 fn test_memory_config_default() {
435 let config = MemoryConfig::default();
440 assert_eq!(config.max_jobs, Some(10_000));
441 assert!(config.auto_cleanup);
442 assert_eq!(config.cleanup_interval, Some(Duration::from_secs(300)));
443 }
444
445 #[test]
446 #[allow(deprecated)]
447 fn test_memory_config_builder() {
448 let config = MemoryConfig::new()
452 .with_max_jobs(5_000)
453 .with_auto_cleanup(false)
454 .with_cleanup_interval(Duration::from_secs(600));
455
456 assert_eq!(config.max_jobs, Some(5_000));
457 assert!(!config.auto_cleanup);
458 assert_eq!(config.cleanup_interval, Some(Duration::from_secs(600)));
459 }
460
461 #[test]
462 #[cfg(feature = "redis")]
463 fn test_redis_config_default() {
464 let config = RedisConfig::default();
465 assert_eq!(config.url, "redis://localhost:6379");
467 assert_eq!(config.pool_size, 10);
468 assert_eq!(config.key_prefix, "qml");
469 assert!(!config.tls);
470 }
471
472 #[test]
473 #[cfg(feature = "redis")]
474 fn test_redis_config_builder() {
475 let config = RedisConfig::new()
476 .with_url("redis://localhost:6380")
477 .with_pool_size(20)
478 .with_key_prefix("test")
479 .with_database(1)
480 .with_credentials("user", "pass")
481 .with_tls(true);
482
483 assert_eq!(config.url, "redis://localhost:6380");
484 assert_eq!(config.pool_size, 20);
485 assert_eq!(config.key_prefix, "test");
486 assert_eq!(config.database, Some(1));
487 assert_eq!(config.username, Some("user".to_string()));
488 assert_eq!(config.password, Some("pass".to_string()));
489 assert!(config.tls);
490 }
491
492 #[test]
493 #[cfg(feature = "redis")]
494 fn test_redis_full_url() {
495 let config = RedisConfig::new()
496 .with_url("redis://localhost:6379")
497 .with_credentials("user", "pass")
498 .with_database(5);
499
500 assert_eq!(config.full_url(), "redis://user:pass@localhost:6379/5");
501 }
502
503 #[test]
504 #[cfg(feature = "redis")]
505 fn test_redis_full_url_password_only() {
506 let config = RedisConfig::new()
507 .with_url("redis://localhost:6379")
508 .with_password("pass")
509 .with_database(2);
510
511 assert_eq!(config.full_url(), "redis://:pass@localhost:6379/2");
512 }
513
514 #[test]
515 #[cfg(feature = "redis")]
516 fn test_storage_config_serialization() {
517 let memory_config = StorageConfig::Memory(MemoryConfig::default());
518 let redis_config = StorageConfig::Redis(RedisConfig::default());
519
520 let memory_json = serde_json::to_string(&memory_config).unwrap();
522 let redis_json = serde_json::to_string(&redis_config).unwrap();
523
524 let _: StorageConfig = serde_json::from_str(&memory_json).unwrap();
525 let _: StorageConfig = serde_json::from_str(&redis_json).unwrap();
526 }
527
528 #[test]
529 #[cfg(feature = "postgres")]
530 fn test_postgres_config_default() {
531 let expected_url = std::env::var("DATABASE_URL")
538 .unwrap_or_else(|_| "postgresql://postgres:password@localhost:5432/qml".to_string());
539 let config = PostgresConfig::default();
540 assert_eq!(config.database_url, expected_url);
541 assert_eq!(config.max_connections, 20);
542 assert_eq!(config.min_connections, 1);
543 assert_eq!(config.table_name, "qml_jobs");
544 assert_eq!(config.schema_name, "qml");
545 assert!(config.auto_migrate);
546 assert!(!config.require_ssl);
547 }
548
549 #[test]
550 #[cfg(feature = "postgres")]
551 fn test_postgres_config_builder() {
552 let config = PostgresConfig::new()
553 .with_database_url("postgresql://user:pass@localhost:5433/testdb")
554 .with_max_connections(50)
555 .with_min_connections(5)
556 .with_table_name("custom_jobs")
557 .with_schema_name("qml")
558 .with_auto_migrate(false)
559 .with_ssl(true);
560
561 assert_eq!(
562 config.database_url,
563 "postgresql://user:pass@localhost:5433/testdb"
564 );
565 assert_eq!(config.max_connections, 50);
566 assert_eq!(config.min_connections, 5);
567 assert_eq!(config.table_name, "custom_jobs");
568 assert_eq!(config.schema_name, "qml");
569 assert!(!config.auto_migrate);
570 assert!(config.require_ssl);
571 }
572
573 #[test]
574 #[cfg(feature = "postgres")]
575 fn test_postgres_full_table_name() {
576 let config = PostgresConfig::new()
577 .with_schema_name("qml")
578 .with_table_name("jobs");
579
580 assert_eq!(config.full_table_name(), "qml.jobs");
581 }
582
583 #[test]
584 #[cfg(feature = "postgres")]
585 fn test_postgres_config_serialization() {
586 let postgres_config = StorageConfig::Postgres(PostgresConfig::default());
587
588 let postgres_json = serde_json::to_string(&postgres_config).unwrap();
590 let _: StorageConfig = serde_json::from_str(&postgres_json).unwrap();
591 }
592}