qml_rs/storage/
config.rs

1use serde::{Deserialize, Serialize};
2use std::time::Duration;
3
4/// Configuration for storage backends
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(tag = "type", rename_all = "lowercase")]
7pub enum StorageConfig {
8    /// In-memory storage configuration
9    Memory(MemoryConfig),
10    /// Redis storage configuration
11    #[cfg(feature = "redis")]
12    Redis(RedisConfig),
13    /// PostgreSQL storage configuration
14    #[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/// Configuration for in-memory storage
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26pub struct MemoryConfig {
27    /// Maximum number of jobs to keep in memory
28    pub max_jobs: Option<usize>,
29    /// Whether to enable auto-cleanup of completed jobs
30    pub auto_cleanup: bool,
31    /// Interval for cleanup operations
32    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)), // 5 minutes
41        }
42    }
43}
44
45impl MemoryConfig {
46    /// Create a new memory config with default settings
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    /// Set the maximum number of jobs to keep in memory
52    pub fn with_max_jobs(mut self, max_jobs: usize) -> Self {
53        self.max_jobs = Some(max_jobs);
54        self
55    }
56
57    /// Disable job limit (unlimited jobs in memory)
58    pub fn unlimited(mut self) -> Self {
59        self.max_jobs = None;
60        self
61    }
62
63    /// Enable auto-cleanup of completed jobs
64    pub fn with_auto_cleanup(mut self, enabled: bool) -> Self {
65        self.auto_cleanup = enabled;
66        self
67    }
68
69    /// Set the cleanup interval
70    pub fn with_cleanup_interval(mut self, interval: Duration) -> Self {
71        self.cleanup_interval = Some(interval);
72        self
73    }
74}
75
76/// Configuration for Redis storage
77#[cfg(feature = "redis")]
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79pub struct RedisConfig {
80    /// Redis connection URL (redis://localhost:6379)
81    pub url: String,
82    /// Connection pool size
83    pub pool_size: u32,
84    /// Connection timeout
85    pub connection_timeout: Duration,
86    /// Command timeout
87    pub command_timeout: Duration,
88    /// Key prefix for all QML keys
89    pub key_prefix: String,
90    /// Database number (0-15 for standard Redis)
91    pub database: Option<u8>,
92    /// Username for authentication (Redis 6.0+)
93    pub username: Option<String>,
94    /// Password for authentication
95    pub password: Option<String>,
96    /// Enable TLS/SSL
97    pub tls: bool,
98    /// TTL for completed jobs (None = no expiration)
99    pub completed_job_ttl: Option<Duration>,
100    /// TTL for failed jobs (None = no expiration)
101    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    /// Create a new Redis config with default settings
130    pub fn new() -> Self {
131        Self::default()
132    }
133    /// Set the Redis connection URL
134    pub fn with_url<S: Into<String>>(mut self, url: S) -> Self {
135        self.url = url.into();
136        self
137    }
138
139    /// Set the connection pool size
140    pub fn with_pool_size(mut self, size: u32) -> Self {
141        self.pool_size = size;
142        self
143    }
144
145    /// Set connection timeout
146    pub fn with_connection_timeout(mut self, timeout: Duration) -> Self {
147        self.connection_timeout = timeout;
148        self
149    }
150
151    /// Set command timeout
152    pub fn with_command_timeout(mut self, timeout: Duration) -> Self {
153        self.command_timeout = timeout;
154        self
155    }
156
157    /// Set the key prefix for QML keys
158    pub fn with_key_prefix<S: Into<String>>(mut self, prefix: S) -> Self {
159        self.key_prefix = prefix.into();
160        self
161    }
162
163    /// Set the Redis database number
164    pub fn with_database(mut self, database: u8) -> Self {
165        self.database = Some(database);
166        self
167    }
168
169    /// Set authentication credentials
170    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    /// Set password for authentication (username will be empty)
181    pub fn with_password<P: Into<String>>(mut self, password: P) -> Self {
182        self.password = Some(password.into());
183        self
184    }
185
186    /// Enable TLS/SSL
187    pub fn with_tls(mut self, enabled: bool) -> Self {
188        self.tls = enabled;
189        self
190    }
191
192    /// Set TTL for completed jobs
193    pub fn with_completed_job_ttl(mut self, ttl: Duration) -> Self {
194        self.completed_job_ttl = Some(ttl);
195        self
196    }
197
198    /// Set TTL for failed jobs
199    pub fn with_failed_job_ttl(mut self, ttl: Duration) -> Self {
200        self.failed_job_ttl = Some(ttl);
201        self
202    }
203
204    /// Disable TTL for completed jobs (keep forever)
205    pub fn no_completed_job_ttl(mut self) -> Self {
206        self.completed_job_ttl = None;
207        self
208    }
209
210    /// Disable TTL for failed jobs (keep forever)
211    pub fn no_failed_job_ttl(mut self) -> Self {
212        self.failed_job_ttl = None;
213        self
214    }
215
216    /// Generate the full Redis URL including credentials and database
217    pub fn full_url(&self) -> String {
218        let mut url = self.url.clone();
219
220        // Add credentials if provided
221        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        // Add database if specified
228        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/// Configuration for PostgreSQL storage
240#[cfg(feature = "postgres")]
241#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
242pub struct PostgresConfig {
243    /// PostgreSQL connection URL (postgresql://user:password@localhost/qml)
244    pub database_url: String,
245    /// Maximum number of connections in the pool
246    pub max_connections: u32,
247    /// Minimum number of connections in the pool
248    pub min_connections: u32,
249    /// Connection timeout
250    pub connect_timeout: Duration,
251    /// Command timeout
252    pub command_timeout: Duration,
253    /// Table name for jobs (default: qml_jobs)
254    pub table_name: String,
255    /// Schema name (default: public)
256    pub schema_name: String,
257    /// Enable automatic migration on startup
258    pub auto_migrate: bool,
259    /// Connection idle timeout
260    pub idle_timeout: Duration,
261    /// Maximum connection lifetime
262    pub max_lifetime: Option<Duration>,
263    /// Enable SSL/TLS
264    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    /// Create a new PostgreSQL config with default settings
291    pub fn new() -> Self {
292        Self::default()
293    }
294    /// Set the database URL
295    pub fn with_database_url<S: Into<String>>(mut self, url: S) -> Self {
296        self.database_url = url.into();
297        self
298    }
299
300    /// Set the migrations path
301    pub fn with_migrations_path<S: Into<String>>(mut self, path: S) -> Self {
302        self.migrations_path = path.into();
303        self
304    }
305
306    /// Set the maximum number of connections in the pool
307    pub fn with_max_connections(mut self, max: u32) -> Self {
308        self.max_connections = max;
309        self
310    }
311
312    /// Set the minimum number of connections in the pool
313    pub fn with_min_connections(mut self, min: u32) -> Self {
314        self.min_connections = min;
315        self
316    }
317
318    /// Set connection timeout
319    pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
320        self.connect_timeout = timeout;
321        self
322    }
323
324    /// Set command timeout
325    pub fn with_command_timeout(mut self, timeout: Duration) -> Self {
326        self.command_timeout = timeout;
327        self
328    }
329
330    /// Set the table name for jobs
331    pub fn with_table_name<S: Into<String>>(mut self, name: S) -> Self {
332        self.table_name = name.into();
333        self
334    }
335
336    /// Set the schema name
337    pub fn with_schema_name<S: Into<String>>(mut self, name: S) -> Self {
338        self.schema_name = name.into();
339        self
340    }
341
342    /// Enable or disable automatic migration
343    pub fn with_auto_migrate(mut self, enabled: bool) -> Self {
344        self.auto_migrate = enabled;
345        self
346    }
347
348    /// Set idle timeout for connections
349    pub fn with_idle_timeout(mut self, timeout: Duration) -> Self {
350        self.idle_timeout = timeout;
351        self
352    }
353
354    /// Set maximum connection lifetime
355    pub fn with_max_lifetime(mut self, lifetime: Duration) -> Self {
356        self.max_lifetime = Some(lifetime);
357        self
358    }
359
360    /// Disable maximum connection lifetime
361    pub fn without_max_lifetime(mut self) -> Self {
362        self.max_lifetime = None;
363        self
364    }
365
366    /// Enable or disable SSL requirement
367    pub fn with_ssl(mut self, require_ssl: bool) -> Self {
368        self.require_ssl = require_ssl;
369        self
370    }
371
372    /// Get the full table name including schema
373    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        // Set the environment variable for the test
406        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        // Clean up the environment variable
415        std::env::remove_var("REDIS_URL");
416    }
417
418    #[test]
419    #[cfg(feature = "redis")]
420    fn test_redis_config_builder() {
421        // Set the environment variable for the test
422        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        // Clean up the environment variable
441        std::env::remove_var("REDIS_URL");
442    }
443
444    #[test]
445    #[cfg(feature = "redis")]
446    fn test_redis_full_url() {
447        // Set the environment variable for the test
448        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        // Clean up the environment variable
458        std::env::remove_var("REDIS_URL");
459    }
460
461    #[test]
462    #[cfg(feature = "redis")]
463    fn test_redis_full_url_password_only() {
464        // Set the environment variable for the test
465        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        // Clean up the environment variable
475        std::env::remove_var("REDIS_URL");
476    }
477
478    #[test]
479    #[cfg(feature = "redis")]
480    fn test_storage_config_serialization() {
481        // Set the environment variable for the test
482        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        // Test that configs can be serialized/deserialized
488        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        // Clean up the environment variable
495        std::env::remove_var("REDIS_URL");
496    }
497
498    #[test]
499    #[cfg(feature = "postgres")]
500    fn test_postgres_config_default() {
501        // Set the environment variable for the test
502        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        // Clean up the environment variable
520        std::env::remove_var("DATABASE_URL");
521    }
522
523    #[test]
524    #[cfg(feature = "postgres")]
525    fn test_postgres_config_builder() {
526        // Set the environment variable for the test
527        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        // Clean up the environment variable
553        std::env::remove_var("DATABASE_URL");
554    }
555
556    #[test]
557    #[cfg(feature = "postgres")]
558    fn test_postgres_full_table_name() {
559        // Set the environment variable for the test
560        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        // Clean up the environment variable
572        std::env::remove_var("DATABASE_URL");
573    }
574
575    #[test]
576    #[cfg(feature = "postgres")]
577    fn test_postgres_config_serialization() {
578        // Set the environment variable for the test
579        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        // Test that config can be serialized/deserialized
587        let postgres_json = serde_json::to_string(&postgres_config).unwrap();
588        let _: StorageConfig = serde_json::from_str(&postgres_json).unwrap();
589
590        // Clean up the environment variable
591        std::env::remove_var("DATABASE_URL");
592    }
593}