rust_task_queue/
config.rs

1//! Configuration management for the task queue framework
2//!
3//! This module provides a comprehensive configuration system that supports:
4//! - Environment variables
5//! - TOML and YAML configuration files
6//! - Global configuration initialization
7//! - Integration with Actix Web
8
9use crate::autoscaler::AutoScalerConfig;
10use crate::TaskQueueError;
11use serde::{Deserialize, Serialize};
12use std::sync::OnceLock;
13
14/// Global configuration instance
15static GLOBAL_CONFIG: OnceLock<TaskQueueConfig> = OnceLock::new();
16
17/// Main configuration structure for the task queue system
18#[derive(Debug, Clone, Serialize, Deserialize, Default)]
19pub struct TaskQueueConfig {
20    /// Redis connection configuration
21    pub redis: RedisConfig,
22
23    /// Worker configuration
24    pub workers: WorkerConfig,
25
26    /// Auto-scaling configuration
27    pub autoscaler: AutoScalerConfig,
28
29    /// Auto-registration settings
30    pub auto_register: AutoRegisterConfig,
31
32    /// Scheduler configuration
33    pub scheduler: SchedulerConfig,
34
35    /// Actix Web integration settings
36    #[cfg(feature = "actix-integration")]
37    pub actix: ActixConfig,
38}
39
40/// Redis connection configuration
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct RedisConfig {
43    /// Redis connection URL
44    pub url: String,
45
46    /// Connection pool size
47    pub pool_size: Option<u32>,
48
49    /// Connection timeout in seconds
50    pub connection_timeout: Option<u64>,
51
52    /// Command timeout in seconds
53    pub command_timeout: Option<u64>,
54}
55
56/// Worker configuration
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct WorkerConfig {
59    /// Initial number of workers to start
60    pub initial_count: usize,
61
62    /// Maximum number of concurrent tasks per worker
63    pub max_concurrent_tasks: Option<usize>,
64
65    /// Worker heartbeat interval in seconds
66    pub heartbeat_interval: Option<u64>,
67
68    /// Grace period for shutdown in seconds
69    pub shutdown_grace_period: Option<u64>,
70}
71
72/// Auto-registration configuration
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct AutoRegisterConfig {
75    /// Enable automatic task registration
76    pub enabled: bool,
77}
78
79/// Scheduler configuration
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct SchedulerConfig {
82    /// Enable the scheduler
83    pub enabled: bool,
84
85    /// Scheduler tick interval in seconds
86    pub tick_interval: Option<u64>,
87
88    /// Maximum number of scheduled tasks to process per tick
89    pub max_tasks_per_tick: Option<usize>,
90}
91
92/// Actix Web integration configuration
93#[cfg(feature = "actix-integration")]
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct ActixConfig {
96    /// Enable automatic route configuration
97    pub auto_configure_routes: bool,
98
99    /// Base path for task queue routes
100    pub route_prefix: String,
101
102    /// Enable metrics endpoint
103    pub enable_metrics: bool,
104
105    /// Enable health check endpoint
106    pub enable_health_check: bool,
107}
108
109impl Default for RedisConfig {
110    fn default() -> Self {
111        Self {
112            url: std::env::var("REDIS_URL")
113                .unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string()),
114            pool_size: None,
115            connection_timeout: None,
116            command_timeout: None,
117        }
118    }
119}
120
121impl Default for WorkerConfig {
122    fn default() -> Self {
123        Self {
124            initial_count: std::env::var("INITIAL_WORKER_COUNT")
125                .ok()
126                .and_then(|s| s.parse().ok())
127                .unwrap_or(2),
128            max_concurrent_tasks: None,
129            heartbeat_interval: None,
130            shutdown_grace_period: None,
131        }
132    }
133}
134
135impl Default for AutoRegisterConfig {
136    fn default() -> Self {
137        Self {
138            enabled: std::env::var("AUTO_REGISTER_TASKS")
139                .map(|s| s.to_lowercase() == "true")
140                .unwrap_or(false),
141        }
142    }
143}
144
145impl Default for SchedulerConfig {
146    fn default() -> Self {
147        Self {
148            enabled: std::env::var("ENABLE_SCHEDULER")
149                .map(|s| s.to_lowercase() == "true")
150                .unwrap_or(false),
151            tick_interval: None,
152            max_tasks_per_tick: None,
153        }
154    }
155}
156
157#[cfg(feature = "actix-integration")]
158impl Default for ActixConfig {
159    fn default() -> Self {
160        Self {
161            auto_configure_routes: std::env::var("AUTO_CONFIGURE_ROUTES")
162                .map(|s| s.to_lowercase() == "true")
163                .unwrap_or(true),
164            route_prefix: std::env::var("ROUTE_PREFIX")
165                .unwrap_or_else(|_| "/api/v1/tasks".to_string()),
166            enable_metrics: std::env::var("ENABLE_METRICS")
167                .map(|s| s.to_lowercase() == "true")
168                .unwrap_or(true),
169            enable_health_check: std::env::var("ENABLE_HEALTH_CHECK")
170                .map(|s| s.to_lowercase() == "true")
171                .unwrap_or(true),
172        }
173    }
174}
175
176impl TaskQueueConfig {
177    /// Validate the entire configuration
178    pub fn validate(&self) -> Result<(), TaskQueueError> {
179        // Validate Redis URL
180        if self.redis.url.is_empty() {
181            return Err(TaskQueueError::Configuration(
182                "Redis URL cannot be empty".to_string(),
183            ));
184        }
185
186        // Basic URL format validation
187        if !self.redis.url.starts_with("redis://") && !self.redis.url.starts_with("rediss://") {
188            return Err(TaskQueueError::Configuration(
189                "Redis URL must start with redis:// or rediss://".to_string(),
190            ));
191        }
192
193        // Validate worker configuration
194        if self.workers.initial_count == 0 {
195            return Err(TaskQueueError::Configuration(
196                "Initial worker count must be greater than 0".to_string(),
197            ));
198        }
199
200        if self.workers.initial_count > 1000 {
201            return Err(TaskQueueError::Configuration(
202                "Initial worker count cannot exceed 1000".to_string(),
203            ));
204        }
205
206        if let Some(max_concurrent) = self.workers.max_concurrent_tasks {
207            if max_concurrent == 0 || max_concurrent > 1000 {
208                return Err(TaskQueueError::Configuration(
209                    "Max concurrent tasks per worker must be between 1 and 1000".to_string(),
210                ));
211            }
212        }
213
214        if let Some(heartbeat) = self.workers.heartbeat_interval {
215            if heartbeat == 0 || heartbeat > 3600 {
216                return Err(TaskQueueError::Configuration(
217                    "Heartbeat interval must be between 1 and 3600 seconds".to_string(),
218                ));
219            }
220        }
221
222        if let Some(grace_period) = self.workers.shutdown_grace_period {
223            if grace_period > 300 {
224                return Err(TaskQueueError::Configuration(
225                    "Shutdown grace period cannot exceed 300 seconds".to_string(),
226                ));
227            }
228        }
229
230        // Validate pool size
231        if let Some(pool_size) = self.redis.pool_size {
232            if pool_size == 0 || pool_size > 1000 {
233                return Err(TaskQueueError::Configuration(
234                    "Redis pool size must be between 1 and 1000".to_string(),
235                ));
236            }
237        }
238
239        // Validate timeouts
240        if let Some(timeout) = self.redis.connection_timeout {
241            if timeout == 0 || timeout > 300 {
242                return Err(TaskQueueError::Configuration(
243                    "Connection timeout must be between 1 and 300 seconds".to_string(),
244                ));
245            }
246        }
247
248        if let Some(timeout) = self.redis.command_timeout {
249            if timeout == 0 || timeout > 300 {
250                return Err(TaskQueueError::Configuration(
251                    "Command timeout must be between 1 and 300 seconds".to_string(),
252                ));
253            }
254        }
255
256        // Validate scheduler configuration
257        if let Some(tick_interval) = self.scheduler.tick_interval {
258            if tick_interval == 0 || tick_interval > 3600 {
259                return Err(TaskQueueError::Configuration(
260                    "Scheduler tick interval must be between 1 and 3600 seconds".to_string(),
261                ));
262            }
263        }
264
265        if let Some(max_tasks) = self.scheduler.max_tasks_per_tick {
266            if max_tasks == 0 || max_tasks > 10000 {
267                return Err(TaskQueueError::Configuration(
268                    "Max tasks per tick must be between 1 and 10000".to_string(),
269                ));
270            }
271        }
272
273        // Validate autoscaler configuration
274        self.autoscaler.validate()?;
275
276        // Validate Actix configuration
277        #[cfg(feature = "actix-integration")]
278        {
279            if self.actix.route_prefix.is_empty() {
280                return Err(TaskQueueError::Configuration(
281                    "Actix route prefix cannot be empty".to_string(),
282                ));
283            }
284
285            if !self.actix.route_prefix.starts_with('/') {
286                return Err(TaskQueueError::Configuration(
287                    "Actix route prefix must start with '/'".to_string(),
288                ));
289            }
290        }
291
292        Ok(())
293    }
294
295    /// Load configuration from environment variables only
296    pub fn from_env() -> Result<Self, TaskQueueError> {
297        let mut config = Self::default();
298
299        // Load additional environment variables that aren't in defaults
300        if let Ok(pool_size) = std::env::var("REDIS_POOL_SIZE") {
301            config.redis.pool_size = Some(pool_size.parse().map_err(|_| {
302                TaskQueueError::Configuration("Invalid REDIS_POOL_SIZE".to_string())
303            })?);
304        }
305
306        if let Ok(timeout) = std::env::var("REDIS_CONNECTION_TIMEOUT") {
307            config.redis.connection_timeout = Some(timeout.parse().map_err(|_| {
308                TaskQueueError::Configuration("Invalid REDIS_CONNECTION_TIMEOUT".to_string())
309            })?);
310        }
311
312        // Validate the configuration
313        config.validate()?;
314
315        Ok(config)
316    }
317
318    /// Load configuration from a file (TOML or YAML based on extension)
319    #[cfg(feature = "config-support")]
320    pub fn from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self, TaskQueueError> {
321        let path = path.as_ref();
322        let contents = std::fs::read_to_string(path).map_err(|e| {
323            TaskQueueError::Configuration(format!("Failed to read config file: {}", e))
324        })?;
325
326        let config: TaskQueueConfig = if path.extension().and_then(|s| s.to_str()) == Some("toml") {
327            toml::from_str(&contents).map_err(|e| {
328                TaskQueueError::Configuration(format!("Failed to parse TOML config: {}", e))
329            })?
330        } else {
331            serde_yaml::from_str(&contents).map_err(|e| {
332                TaskQueueError::Configuration(format!("Failed to parse YAML config: {}", e))
333            })?
334        };
335
336        // Validate the configuration
337        config.validate()?;
338
339        Ok(config)
340    }
341
342    /// Load configuration with automatic source detection and validation
343    #[cfg(feature = "config-support")]
344    pub fn load() -> Result<Self, TaskQueueError> {
345        use config::{Config, Environment, File};
346
347        let mut builder = Config::builder()
348            // Start with defaults
349            .add_source(Config::try_from(&Self::default()).map_err(|e| {
350                TaskQueueError::Configuration(format!("Failed to create default config: {}", e))
351            })?);
352
353        // Check for config file in common locations
354        for config_path in &[
355            "task-queue.toml",
356            "task-queue.yaml",
357            "task-queue.yml",
358            "config/task-queue.toml",
359            "config/task-queue.yaml",
360            "config/task-queue.yml",
361        ] {
362            if std::path::Path::new(config_path).exists() {
363                builder = builder.add_source(File::with_name(config_path));
364                break;
365            }
366        }
367
368        // Override with environment variables (prefixed with TASK_QUEUE_)
369        builder = builder.add_source(
370            Environment::with_prefix("TASK_QUEUE")
371                .separator("_")
372                .try_parsing(true),
373        );
374
375        // Also support unprefixed common variables like REDIS_URL
376        if let Ok(redis_url) = std::env::var("REDIS_URL") {
377            builder = builder.set_override("redis.url", redis_url).map_err(|e| {
378                TaskQueueError::Configuration(format!("Failed to set REDIS_URL override: {}", e))
379            })?;
380        }
381
382        let config = builder
383            .build()
384            .map_err(|e| TaskQueueError::Configuration(format!("Failed to build config: {}", e)))?;
385
386        let config: TaskQueueConfig = config.try_deserialize().map_err(|e| {
387            TaskQueueError::Configuration(format!("Failed to deserialize config: {}", e))
388        })?;
389
390        // Validate the configuration
391        config.validate()?;
392
393        Ok(config)
394    }
395
396    /// Load configuration without the config crate (fallback)
397    #[cfg(not(feature = "config-support"))]
398    pub fn load() -> Result<Self, TaskQueueError> {
399        Self::from_env()
400    }
401
402    /// Initialize global configuration if not already done
403    pub fn init_global() -> Result<&'static Self, TaskQueueError> {
404        match GLOBAL_CONFIG.get() {
405            Some(config) => Ok(config),
406            None => match GLOBAL_CONFIG.set(Self::load()?) {
407                Ok(()) => Ok(GLOBAL_CONFIG.get().unwrap()),
408                Err(_) => Ok(GLOBAL_CONFIG.get().unwrap()),
409            },
410        }
411    }
412
413    /// Get global configuration (if initialized)
414    pub fn global() -> Option<&'static Self> {
415        GLOBAL_CONFIG.get()
416    }
417
418    /// Get global configuration or initialize it
419    pub fn get_or_init() -> Result<&'static Self, TaskQueueError> {
420        match GLOBAL_CONFIG.get() {
421            Some(config) => Ok(config),
422            None => Self::init_global(),
423        }
424    }
425}
426
427/// Configuration builder for fluent API
428#[derive(Default)]
429pub struct ConfigBuilder {
430    config: TaskQueueConfig,
431}
432
433impl ConfigBuilder {
434    pub fn new() -> Self {
435        Self::default()
436    }
437
438    pub fn redis_url(mut self, url: impl Into<String>) -> Self {
439        self.config.redis.url = url.into();
440        self
441    }
442
443    pub fn workers(mut self, count: usize) -> Self {
444        self.config.workers.initial_count = count;
445        self
446    }
447
448    pub fn enable_auto_register(mut self, enabled: bool) -> Self {
449        self.config.auto_register.enabled = enabled;
450        self
451    }
452
453    pub fn enable_scheduler(mut self, enabled: bool) -> Self {
454        self.config.scheduler.enabled = enabled;
455        self
456    }
457
458    pub fn autoscaler_config(mut self, config: AutoScalerConfig) -> Self {
459        self.config.autoscaler = config;
460        self
461    }
462
463    pub fn build(self) -> TaskQueueConfig {
464        self.config
465    }
466}
467
468#[cfg(test)]
469mod tests {
470    use super::*;
471    use std::env;
472
473    #[test]
474    fn test_redis_config_default() {
475        let config = RedisConfig::default();
476
477        // Should use environment variable or default
478        assert!(!config.url.is_empty());
479        assert!(config.url.starts_with("redis://"));
480        assert!(config.pool_size.is_none());
481        assert!(config.connection_timeout.is_none());
482        assert!(config.command_timeout.is_none());
483    }
484
485    #[test]
486    fn test_worker_config_default() {
487        let config = WorkerConfig::default();
488
489        assert!(config.initial_count > 0);
490        assert!(config.max_concurrent_tasks.is_none());
491        assert!(config.heartbeat_interval.is_none());
492        assert!(config.shutdown_grace_period.is_none());
493    }
494
495    #[test]
496    fn test_auto_register_config_default() {
497        let config = AutoRegisterConfig::default();
498        // Default should be false unless environment variable is set
499        assert!(!config.enabled || env::var("AUTO_REGISTER_TASKS").is_ok());
500    }
501
502    #[test]
503    fn test_scheduler_config_default() {
504        let config = SchedulerConfig::default();
505        // Default should be false unless environment variable is set
506        assert!(!config.enabled || env::var("ENABLE_SCHEDULER").is_ok());
507        assert!(config.tick_interval.is_none());
508        assert!(config.max_tasks_per_tick.is_none());
509    }
510
511    #[cfg(feature = "actix-integration")]
512    #[test]
513    fn test_actix_config_default() {
514        let config = ActixConfig::default();
515
516        assert!(!config.route_prefix.is_empty());
517        assert!(config.route_prefix.starts_with('/'));
518        // Other defaults may vary based on environment variables
519    }
520
521    #[test]
522    fn test_task_queue_config_default() {
523        let config = TaskQueueConfig::default();
524
525        assert!(!config.redis.url.is_empty());
526        assert!(config.workers.initial_count > 0);
527        // Other fields should have sensible defaults
528    }
529
530    #[test]
531    fn test_config_validation_valid() {
532        let config = TaskQueueConfig {
533            redis: RedisConfig {
534                url: "redis://localhost:6379".to_string(),
535                pool_size: Some(10),
536                connection_timeout: Some(30),
537                command_timeout: Some(60),
538            },
539            workers: WorkerConfig {
540                initial_count: 4,
541                max_concurrent_tasks: Some(10),
542                heartbeat_interval: Some(30),
543                shutdown_grace_period: Some(60),
544            },
545            autoscaler: AutoScalerConfig::default(),
546            auto_register: AutoRegisterConfig { enabled: true },
547            scheduler: SchedulerConfig {
548                enabled: true,
549                tick_interval: Some(60),
550                max_tasks_per_tick: Some(100),
551            },
552            #[cfg(feature = "actix-integration")]
553            actix: ActixConfig {
554                auto_configure_routes: true,
555                route_prefix: "/api/tasks".to_string(),
556                enable_metrics: true,
557                enable_health_check: true,
558            },
559        };
560
561        assert!(config.validate().is_ok());
562    }
563
564    #[test]
565    fn test_config_validation_empty_redis_url() {
566        let mut config = TaskQueueConfig::default();
567        config.redis.url = "".to_string();
568
569        let result = config.validate();
570        assert!(result.is_err());
571        assert!(result
572            .unwrap_err()
573            .to_string()
574            .contains("Redis URL cannot be empty"));
575    }
576
577    #[test]
578    fn test_config_validation_invalid_redis_url() {
579        let mut config = TaskQueueConfig::default();
580        config.redis.url = "http://localhost:6379".to_string();
581
582        let result = config.validate();
583        assert!(result.is_err());
584        assert!(result
585            .unwrap_err()
586            .to_string()
587            .contains("Redis URL must start with redis://"));
588    }
589
590    #[test]
591    fn test_config_validation_zero_workers() {
592        let mut config = TaskQueueConfig::default();
593        config.workers.initial_count = 0;
594
595        let result = config.validate();
596        assert!(result.is_err());
597        assert!(result
598            .unwrap_err()
599            .to_string()
600            .contains("Initial worker count must be greater than 0"));
601    }
602
603    #[test]
604    fn test_config_validation_too_many_workers() {
605        let mut config = TaskQueueConfig::default();
606        config.workers.initial_count = 1001;
607
608        let result = config.validate();
609        assert!(result.is_err());
610        assert!(result
611            .unwrap_err()
612            .to_string()
613            .contains("Initial worker count cannot exceed 1000"));
614    }
615
616    #[test]
617    fn test_config_validation_invalid_pool_size() {
618        let mut config = TaskQueueConfig::default();
619        config.redis.pool_size = Some(0);
620
621        let result = config.validate();
622        assert!(result.is_err());
623        assert!(result
624            .unwrap_err()
625            .to_string()
626            .contains("Redis pool size must be between 1 and 1000"));
627    }
628
629    #[test]
630    fn test_config_validation_invalid_timeouts() {
631        let mut config = TaskQueueConfig::default();
632        config.redis.connection_timeout = Some(0);
633
634        let result = config.validate();
635        assert!(result.is_err());
636        assert!(result
637            .unwrap_err()
638            .to_string()
639            .contains("Connection timeout must be between 1 and 300"));
640
641        config.redis.connection_timeout = Some(30);
642        config.redis.command_timeout = Some(301);
643
644        let result = config.validate();
645        assert!(result.is_err());
646        assert!(result
647            .unwrap_err()
648            .to_string()
649            .contains("Command timeout must be between 1 and 300"));
650    }
651
652    #[test]
653    fn test_config_validation_invalid_worker_settings() {
654        let mut config = TaskQueueConfig::default();
655        config.workers.max_concurrent_tasks = Some(0);
656
657        let result = config.validate();
658        assert!(result.is_err());
659        assert!(result
660            .unwrap_err()
661            .to_string()
662            .contains("Max concurrent tasks per worker must be between 1 and 1000"));
663
664        config.workers.max_concurrent_tasks = Some(10);
665        config.workers.heartbeat_interval = Some(0);
666
667        let result = config.validate();
668        assert!(result.is_err());
669        assert!(result
670            .unwrap_err()
671            .to_string()
672            .contains("Heartbeat interval must be between 1 and 3600"));
673
674        config.workers.heartbeat_interval = Some(30);
675        config.workers.shutdown_grace_period = Some(301);
676
677        let result = config.validate();
678        assert!(result.is_err());
679        assert!(result
680            .unwrap_err()
681            .to_string()
682            .contains("Shutdown grace period cannot exceed 300"));
683    }
684
685    #[test]
686    fn test_config_validation_invalid_scheduler_settings() {
687        let mut config = TaskQueueConfig::default();
688        config.scheduler.tick_interval = Some(0);
689
690        let result = config.validate();
691        assert!(result.is_err());
692        assert!(result
693            .unwrap_err()
694            .to_string()
695            .contains("Scheduler tick interval must be between 1 and 3600"));
696
697        config.scheduler.tick_interval = Some(60);
698        config.scheduler.max_tasks_per_tick = Some(0);
699
700        let result = config.validate();
701        assert!(result.is_err());
702        assert!(result
703            .unwrap_err()
704            .to_string()
705            .contains("Max tasks per tick must be between 1 and 10000"));
706    }
707
708    #[cfg(feature = "actix-integration")]
709    #[test]
710    fn test_config_validation_invalid_actix_settings() {
711        let mut config = TaskQueueConfig::default();
712        config.actix.route_prefix = "".to_string();
713
714        let result = config.validate();
715        assert!(result.is_err());
716        assert!(result
717            .unwrap_err()
718            .to_string()
719            .contains("Actix route prefix cannot be empty"));
720
721        config.actix.route_prefix = "api/tasks".to_string(); // Missing leading slash
722
723        let result = config.validate();
724        assert!(result.is_err());
725        assert!(result
726            .unwrap_err()
727            .to_string()
728            .contains("Actix route prefix must start with '/'"));
729    }
730
731    #[test]
732    fn test_config_builder() {
733        let config = ConfigBuilder::new()
734            .redis_url("redis://test:6379")
735            .workers(8)
736            .enable_auto_register(true)
737            .enable_scheduler(true)
738            .autoscaler_config(AutoScalerConfig {
739                min_workers: 2,
740                max_workers: 16,
741                scale_up_count: 4,
742                scale_down_count: 2,
743                scaling_triggers: crate::autoscaler::ScalingTriggers {
744                    queue_pressure_threshold: 1.0,
745                    worker_utilization_threshold: 0.85,
746                    task_complexity_threshold: 1.5,
747                    error_rate_threshold: 0.05,
748                    memory_pressure_threshold: 512.0,
749                },
750                enable_adaptive_thresholds: true,
751                learning_rate: 0.1,
752                adaptation_window_minutes: 30,
753                scale_up_cooldown_seconds: 60,
754                scale_down_cooldown_seconds: 300,
755                consecutive_signals_required: 2,
756                target_sla: crate::autoscaler::SLATargets {
757                    max_p95_latency_ms: 5000.0,
758                    min_success_rate: 0.95,
759                    max_queue_wait_time_ms: 10000.0,
760                    target_worker_utilization: 0.70,
761                },
762            })
763            .build();
764
765        assert_eq!(config.redis.url, "redis://test:6379");
766        assert_eq!(config.workers.initial_count, 8);
767        assert!(config.auto_register.enabled);
768        assert!(config.scheduler.enabled);
769        assert_eq!(config.autoscaler.min_workers, 2);
770        assert_eq!(config.autoscaler.max_workers, 16);
771    }
772
773    #[test]
774    fn test_config_serialization() {
775        let config = TaskQueueConfig::default();
776
777        // Test JSON serialization
778        let json = serde_json::to_string(&config).expect("Failed to serialize to JSON");
779        let deserialized: TaskQueueConfig =
780            serde_json::from_str(&json).expect("Failed to deserialize from JSON");
781
782        assert_eq!(config.redis.url, deserialized.redis.url);
783        assert_eq!(
784            config.workers.initial_count,
785            deserialized.workers.initial_count
786        );
787        assert_eq!(
788            config.auto_register.enabled,
789            deserialized.auto_register.enabled
790        );
791        assert_eq!(config.scheduler.enabled, deserialized.scheduler.enabled);
792    }
793
794    #[test]
795    fn test_config_from_env() {
796        // Set environment variables
797        env::set_var("REDIS_POOL_SIZE", "15");
798        env::set_var("REDIS_CONNECTION_TIMEOUT", "45");
799
800        let config = TaskQueueConfig::from_env().expect("Failed to load config from env");
801
802        assert_eq!(config.redis.pool_size, Some(15));
803        assert_eq!(config.redis.connection_timeout, Some(45));
804
805        // Clean up
806        env::remove_var("REDIS_POOL_SIZE");
807        env::remove_var("REDIS_CONNECTION_TIMEOUT");
808    }
809
810    #[test]
811    fn test_config_from_env_invalid_values() {
812        // Set invalid environment variable
813        env::set_var("REDIS_POOL_SIZE", "invalid");
814
815        let result = TaskQueueConfig::from_env();
816        assert!(result.is_err());
817        assert!(result
818            .unwrap_err()
819            .to_string()
820            .contains("Invalid REDIS_POOL_SIZE"));
821
822        // Clean up
823        env::remove_var("REDIS_POOL_SIZE");
824    }
825
826    #[test]
827    fn test_config_clone() {
828        let original = TaskQueueConfig::default();
829        let cloned = original.clone();
830
831        assert_eq!(original.redis.url, cloned.redis.url);
832        assert_eq!(original.workers.initial_count, cloned.workers.initial_count);
833        assert_eq!(original.auto_register.enabled, cloned.auto_register.enabled);
834        assert_eq!(original.scheduler.enabled, cloned.scheduler.enabled);
835    }
836
837    #[test]
838    fn test_config_debug() {
839        let config = TaskQueueConfig::default();
840        let debug_str = format!("{:?}", config);
841
842        assert!(debug_str.contains("TaskQueueConfig"));
843        assert!(debug_str.contains("redis"));
844        assert!(debug_str.contains("workers"));
845        assert!(debug_str.contains("autoscaler"));
846    }
847
848    #[test]
849    fn test_individual_config_structs_clone() {
850        let redis_config = RedisConfig::default();
851        let cloned_redis = redis_config.clone();
852        assert_eq!(redis_config.url, cloned_redis.url);
853
854        let worker_config = WorkerConfig::default();
855        let cloned_worker = worker_config.clone();
856        assert_eq!(worker_config.initial_count, cloned_worker.initial_count);
857
858        let auto_register_config = AutoRegisterConfig::default();
859        let cloned_auto_register = auto_register_config.clone();
860        assert_eq!(auto_register_config.enabled, cloned_auto_register.enabled);
861
862        let scheduler_config = SchedulerConfig::default();
863        let cloned_scheduler = scheduler_config.clone();
864        assert_eq!(scheduler_config.enabled, cloned_scheduler.enabled);
865    }
866
867    #[test]
868    fn test_config_builder_default() {
869        let builder = ConfigBuilder::new();
870        let config = builder.build();
871
872        // Should have the same values as TaskQueueConfig::default()
873        let default_config = TaskQueueConfig::default();
874        assert_eq!(config.redis.url, default_config.redis.url);
875        assert_eq!(
876            config.workers.initial_count,
877            default_config.workers.initial_count
878        );
879    }
880
881    #[test]
882    fn test_config_builder_method_chaining() {
883        let config = ConfigBuilder::default()
884            .redis_url("redis://chained:6379")
885            .workers(5)
886            .enable_auto_register(false)
887            .enable_scheduler(false)
888            .build();
889
890        assert_eq!(config.redis.url, "redis://chained:6379");
891        assert_eq!(config.workers.initial_count, 5);
892        assert!(!config.auto_register.enabled);
893        assert!(!config.scheduler.enabled);
894    }
895
896    #[test]
897    fn test_redis_url_validation() {
898        let test_cases = vec![
899            ("redis://localhost:6379", true),
900            ("rediss://localhost:6379", true),
901            ("redis://user:pass@localhost:6379/0", true),
902            ("redis://localhost", true),
903            ("http://localhost:6379", false),
904            ("localhost:6379", false),
905            ("", false),
906        ];
907
908        for (url, should_be_valid) in test_cases {
909            let mut config = TaskQueueConfig::default();
910            config.redis.url = url.to_string();
911
912            let result = config.validate();
913            if should_be_valid {
914                assert!(result.is_ok(), "URL '{}' should be valid", url);
915            } else {
916                assert!(result.is_err(), "URL '{}' should be invalid", url);
917            }
918        }
919    }
920}