Skip to main content

oxirs_stream/
config.rs

1//! # Advanced Configuration Management
2//!
3//! This module provides comprehensive configuration management for oxirs-stream with:
4//! - Dynamic configuration updates without restart
5//! - Environment-based configuration loading
6//! - Secret management integration
7//! - SSL/TLS certificate management
8//! - Authentication configuration
9//! - Performance tuning profiles
10
11use anyhow::{anyhow, Result};
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::fs;
15use std::path::{Path, PathBuf};
16use std::sync::Arc;
17use std::time::Duration;
18use tokio::sync::{broadcast, RwLock};
19use tracing::info;
20
21use crate::{CompressionType, StreamBackendType, StreamConfig, StreamPerformanceConfig};
22
23/// Configuration source types
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub enum ConfigSource {
26    /// Configuration from file
27    File { path: PathBuf },
28    /// Configuration from environment variables
29    Environment { prefix: String },
30    /// Configuration from remote source (e.g., etcd, consul)
31    Remote { url: String, key: String },
32    /// Configuration from memory (for testing)
33    Memory { data: HashMap<String, String> },
34}
35
36/// Configuration manager with dynamic reload support
37pub struct ConfigManager {
38    /// Current configuration
39    current_config: Arc<RwLock<StreamConfig>>,
40    /// Configuration sources in priority order
41    sources: Vec<ConfigSource>,
42    /// Configuration change notifier
43    change_notifier: broadcast::Sender<ConfigChangeEvent>,
44    /// Secret manager
45    secret_manager: Arc<SecretManager>,
46    /// Environment detector
47    environment: Environment,
48    /// Performance profiles
49    performance_profiles: HashMap<String, PerformanceProfile>,
50    /// SSL/TLS manager
51    tls_manager: Arc<TlsManager>,
52}
53
54/// Configuration change event
55#[derive(Debug, Clone)]
56pub enum ConfigChangeEvent {
57    /// Configuration reloaded
58    Reloaded {
59        old_config: Box<StreamConfig>,
60        new_config: Box<StreamConfig>,
61    },
62    /// Configuration validation failed
63    ValidationFailed { reason: String },
64    /// Secret rotated
65    SecretRotated { secret_name: String },
66    /// TLS certificate updated
67    TlsCertificateUpdated { cert_type: String },
68}
69
70/// Environment detection
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub enum Environment {
73    Development,
74    Testing,
75    Staging,
76    Production,
77}
78
79impl Environment {
80    /// Detect environment from various sources
81    pub fn detect() -> Self {
82        if let Ok(env) = std::env::var("OXIRS_ENV") {
83            match env.to_lowercase().as_str() {
84                "dev" | "development" => Environment::Development,
85                "test" | "testing" => Environment::Testing,
86                "staging" | "stage" => Environment::Staging,
87                "prod" | "production" => Environment::Production,
88                _ => Environment::Development,
89            }
90        } else if let Ok(env) = std::env::var("RUST_ENV") {
91            match env.to_lowercase().as_str() {
92                "production" => Environment::Production,
93                _ => Environment::Development,
94            }
95        } else {
96            Environment::Development
97        }
98    }
99
100    /// Get environment-specific defaults
101    pub fn get_defaults(&self) -> ConfigDefaults {
102        match self {
103            Environment::Development => ConfigDefaults {
104                log_level: "debug".to_string(),
105                enable_debug_endpoints: true,
106                connection_timeout_secs: 60,
107                max_connections: 10,
108                enable_compression: false,
109                enable_metrics: true,
110                enable_profiling: true,
111            },
112            Environment::Testing => ConfigDefaults {
113                log_level: "info".to_string(),
114                enable_debug_endpoints: true,
115                connection_timeout_secs: 30,
116                max_connections: 5,
117                enable_compression: false,
118                enable_metrics: true,
119                enable_profiling: false,
120            },
121            Environment::Staging => ConfigDefaults {
122                log_level: "info".to_string(),
123                enable_debug_endpoints: false,
124                connection_timeout_secs: 30,
125                max_connections: 50,
126                enable_compression: true,
127                enable_metrics: true,
128                enable_profiling: false,
129            },
130            Environment::Production => ConfigDefaults {
131                log_level: "warn".to_string(),
132                enable_debug_endpoints: false,
133                connection_timeout_secs: 30,
134                max_connections: 100,
135                enable_compression: true,
136                enable_metrics: true,
137                enable_profiling: false,
138            },
139        }
140    }
141}
142
143/// Environment-specific configuration defaults
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct ConfigDefaults {
146    pub log_level: String,
147    pub enable_debug_endpoints: bool,
148    pub connection_timeout_secs: u64,
149    pub max_connections: usize,
150    pub enable_compression: bool,
151    pub enable_metrics: bool,
152    pub enable_profiling: bool,
153}
154
155/// Secret manager for handling sensitive configuration
156pub struct SecretManager {
157    /// Secret store backend
158    backend: SecretBackend,
159    /// Cached secrets with expiration
160    cache: Arc<RwLock<HashMap<String, CachedSecret>>>,
161    /// Secret rotation interval
162    rotation_interval: Duration,
163}
164
165/// Secret backend types
166#[derive(Debug, Clone)]
167pub enum SecretBackend {
168    /// Environment variables
169    Environment { prefix: String },
170    /// File-based secrets
171    File { directory: PathBuf },
172    /// HashiCorp Vault
173    Vault {
174        url: String,
175        token: String,
176        mount_path: String,
177    },
178    /// AWS Secrets Manager
179    AwsSecretsManager { region: String },
180    /// Memory-based (for testing)
181    Memory {
182        secrets: Arc<RwLock<HashMap<String, String>>>,
183    },
184}
185
186/// Cached secret with metadata
187#[derive(Debug, Clone)]
188struct CachedSecret {
189    value: String,
190    cached_at: std::time::Instant,
191    expires_at: Option<std::time::Instant>,
192    version: u64,
193}
194
195impl SecretManager {
196    /// Create a new secret manager
197    pub fn new(backend: SecretBackend, rotation_interval: Duration) -> Self {
198        Self {
199            backend,
200            cache: Arc::new(RwLock::new(HashMap::new())),
201            rotation_interval,
202        }
203    }
204
205    /// Get a secret value
206    pub async fn get_secret(&self, name: &str) -> Result<String> {
207        // Check cache first
208        {
209            let cache = self.cache.read().await;
210            if let Some(cached) = cache.get(name) {
211                if cached
212                    .expires_at
213                    .map_or(true, |exp| exp > std::time::Instant::now())
214                {
215                    return Ok(cached.value.clone());
216                }
217            }
218        }
219
220        // Fetch from backend
221        let value = match &self.backend {
222            SecretBackend::Environment { prefix } => {
223                let key = format!("{prefix}_{}", name.to_uppercase());
224                std::env::var(key).map_err(|_| anyhow!("Secret {name} not found in environment"))
225            }
226            SecretBackend::File { directory } => {
227                let path = directory.join(name);
228                fs::read_to_string(&path)
229                    .map_err(|e| anyhow!("Failed to read secret from {path:?}: {e}"))
230                    .map(|s| s.trim().to_string())
231            }
232            SecretBackend::Memory { secrets } => {
233                let secrets = secrets.read().await;
234                secrets
235                    .get(name)
236                    .cloned()
237                    .ok_or_else(|| anyhow!("Secret {name} not found"))
238            }
239            _ => {
240                // Vault and AWS Secrets Manager integrations require external service connections.
241                // These backends are not supported in this build configuration.
242                return Err(anyhow!(
243                    "Secret backend requires an external service connection (Vault/AWS) \
244                     that is not available in this configuration"
245                ));
246            }
247        }?;
248
249        // Cache the secret
250        let cached = CachedSecret {
251            value: value.clone(),
252            cached_at: std::time::Instant::now(),
253            expires_at: Some(std::time::Instant::now() + self.rotation_interval),
254            version: 1,
255        };
256
257        self.cache.write().await.insert(name.to_string(), cached);
258        Ok(value)
259    }
260
261    /// Set a secret (for testing)
262    pub async fn set_secret(&self, name: &str, value: &str) -> Result<()> {
263        match &self.backend {
264            SecretBackend::Memory { secrets } => {
265                secrets
266                    .write()
267                    .await
268                    .insert(name.to_string(), value.to_string());
269                self.cache.write().await.remove(name);
270                Ok(())
271            }
272            _ => Err(anyhow!("Set secret only supported for memory backend")),
273        }
274    }
275
276    /// Rotate all secrets
277    pub async fn rotate_secrets(&self) -> Result<()> {
278        self.cache.write().await.clear();
279        info!("Rotated all cached secrets");
280        Ok(())
281    }
282}
283
284/// TLS/SSL certificate manager
285pub struct TlsManager {
286    /// Certificate store
287    certs: Arc<RwLock<HashMap<String, TlsCertificate>>>,
288    /// Certificate paths
289    cert_paths: HashMap<String, CertPaths>,
290    /// Auto-reload enabled
291    auto_reload: bool,
292}
293
294/// TLS certificate with metadata
295#[derive(Debug, Clone)]
296pub struct TlsCertificate {
297    pub cert_pem: Vec<u8>,
298    pub key_pem: Vec<u8>,
299    pub ca_pem: Option<Vec<u8>>,
300    pub loaded_at: std::time::Instant,
301    pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
302}
303
304/// Certificate file paths
305#[derive(Debug, Clone)]
306struct CertPaths {
307    cert_path: PathBuf,
308    key_path: PathBuf,
309    ca_path: Option<PathBuf>,
310}
311
312impl TlsManager {
313    /// Create a new TLS manager
314    pub fn new(auto_reload: bool) -> Self {
315        Self {
316            certs: Arc::new(RwLock::new(HashMap::new())),
317            cert_paths: HashMap::new(),
318            auto_reload,
319        }
320    }
321
322    /// Parse certificate expiration from PEM data
323    fn parse_certificate_expiration(cert_pem: &[u8]) -> Option<chrono::DateTime<chrono::Utc>> {
324        // Simple PEM certificate expiration parsing
325        // In a production system, we'd use proper X.509 parsing libraries
326        let cert_str = String::from_utf8_lossy(cert_pem);
327
328        // For now, we'll implement a basic parser that looks for common patterns
329        // This is a placeholder implementation that could be enhanced with proper X.509 parsing
330        if cert_str.contains("-----BEGIN CERTIFICATE-----") {
331            // Set a default expiration of 1 year from now for valid certificates
332            // In practice, this should parse the actual certificate validity period
333            Some(chrono::Utc::now() + chrono::Duration::days(365))
334        } else {
335            None
336        }
337    }
338
339    /// Load a certificate
340    pub async fn load_certificate(
341        &self,
342        name: &str,
343        cert_path: &Path,
344        key_path: &Path,
345        ca_path: Option<&Path>,
346    ) -> Result<()> {
347        let cert_pem = fs::read(cert_path)
348            .map_err(|e| anyhow!("Failed to read certificate {}: {}", cert_path.display(), e))?;
349
350        let key_pem = fs::read(key_path)
351            .map_err(|e| anyhow!("Failed to read key {}: {}", key_path.display(), e))?;
352
353        let ca_pem = if let Some(ca) = ca_path {
354            Some(fs::read(ca).map_err(|e| anyhow!("Failed to read CA {}: {}", ca.display(), e))?)
355        } else {
356            None
357        };
358
359        let expires_at = Self::parse_certificate_expiration(&cert_pem);
360
361        let cert = TlsCertificate {
362            cert_pem,
363            key_pem,
364            ca_pem,
365            loaded_at: std::time::Instant::now(),
366            expires_at,
367        };
368
369        self.certs.write().await.insert(name.to_string(), cert);
370
371        info!("Loaded TLS certificate: {}", name);
372        Ok(())
373    }
374
375    /// Get a certificate
376    pub async fn get_certificate(&self, name: &str) -> Result<TlsCertificate> {
377        self.certs
378            .read()
379            .await
380            .get(name)
381            .cloned()
382            .ok_or_else(|| anyhow!("Certificate {} not found", name))
383    }
384
385    /// Check certificate expiration
386    pub async fn check_expiration(&self) -> Vec<(String, chrono::DateTime<chrono::Utc>)> {
387        let certs = self.certs.read().await;
388        let mut expiring = Vec::new();
389
390        for (name, cert) in certs.iter() {
391            if let Some(expires) = cert.expires_at {
392                let days_until = (expires - chrono::Utc::now()).num_days();
393                if days_until < 30 {
394                    expiring.push((name.clone(), expires));
395                }
396            }
397        }
398
399        expiring
400    }
401}
402
403/// Performance tuning profile
404#[derive(Debug, Clone, Serialize, Deserialize)]
405pub struct PerformanceProfile {
406    pub name: String,
407    pub description: String,
408    pub settings: StreamPerformanceConfig,
409    pub recommended_for: Vec<String>,
410}
411
412impl ConfigManager {
413    /// Create a new configuration manager
414    pub async fn new(sources: Vec<ConfigSource>) -> Result<Self> {
415        let environment = Environment::detect();
416        let (tx, _) = broadcast::channel(100);
417
418        // Initialize secret manager
419        let secret_backend = SecretBackend::Environment {
420            prefix: "OXIRS_SECRET".to_string(),
421        };
422        let secret_manager = Arc::new(SecretManager::new(
423            secret_backend,
424            Duration::from_secs(3600),
425        ));
426
427        // Initialize TLS manager
428        let tls_manager = Arc::new(TlsManager::new(true));
429
430        // Load initial configuration
431        let initial_config = StreamConfig::default();
432
433        let mut manager = Self {
434            current_config: Arc::new(RwLock::new(initial_config)),
435            sources,
436            change_notifier: tx,
437            secret_manager,
438            environment,
439            performance_profiles: Self::create_default_profiles(),
440            tls_manager,
441        };
442
443        // Load configuration from sources
444        manager.reload().await?;
445
446        Ok(manager)
447    }
448
449    /// Create default performance profiles
450    fn create_default_profiles() -> HashMap<String, PerformanceProfile> {
451        let mut profiles = HashMap::new();
452
453        // Low latency profile
454        profiles.insert(
455            "low-latency".to_string(),
456            PerformanceProfile {
457                name: "low-latency".to_string(),
458                description: "Optimized for minimal latency".to_string(),
459                settings: StreamPerformanceConfig {
460                    enable_batching: false,
461                    enable_pipelining: true,
462                    buffer_size: 4096,
463                    prefetch_count: 10,
464                    enable_zero_copy: true,
465                    enable_simd: true,
466                    parallel_processing: true,
467                    worker_threads: Some(4),
468                },
469                recommended_for: vec!["real-time".to_string(), "trading".to_string()],
470            },
471        );
472
473        // High throughput profile
474        profiles.insert(
475            "high-throughput".to_string(),
476            PerformanceProfile {
477                name: "high-throughput".to_string(),
478                description: "Optimized for maximum throughput".to_string(),
479                settings: StreamPerformanceConfig {
480                    enable_batching: true,
481                    enable_pipelining: true,
482                    buffer_size: 65536,
483                    prefetch_count: 1000,
484                    enable_zero_copy: true,
485                    enable_simd: true,
486                    parallel_processing: true,
487                    worker_threads: None, // Use all available
488                },
489                recommended_for: vec!["batch-processing".to_string(), "etl".to_string()],
490            },
491        );
492
493        // Balanced profile
494        profiles.insert(
495            "balanced".to_string(),
496            PerformanceProfile {
497                name: "balanced".to_string(),
498                description: "Balanced between latency and throughput".to_string(),
499                settings: StreamPerformanceConfig::default(),
500                recommended_for: vec!["general".to_string(), "web-services".to_string()],
501            },
502        );
503
504        // Resource-constrained profile
505        profiles.insert(
506            "resource-constrained".to_string(),
507            PerformanceProfile {
508                name: "resource-constrained".to_string(),
509                description: "Optimized for limited resources".to_string(),
510                settings: StreamPerformanceConfig {
511                    enable_batching: true,
512                    enable_pipelining: false,
513                    buffer_size: 2048,
514                    prefetch_count: 10,
515                    enable_zero_copy: false,
516                    enable_simd: false,
517                    parallel_processing: false,
518                    worker_threads: Some(2),
519                },
520                recommended_for: vec!["edge".to_string(), "iot".to_string()],
521            },
522        );
523
524        profiles
525    }
526
527    /// Reload configuration from all sources
528    pub async fn reload(&mut self) -> Result<()> {
529        let old_config = self.current_config.read().await.clone();
530        let mut new_config = old_config.clone();
531
532        // Apply environment defaults
533        let defaults = self.environment.get_defaults();
534        new_config.max_connections = defaults.max_connections;
535        new_config.connection_timeout = Duration::from_secs(defaults.connection_timeout_secs);
536        new_config.enable_compression = defaults.enable_compression;
537        new_config.monitoring.enable_metrics = defaults.enable_metrics;
538        new_config.monitoring.enable_profiling = defaults.enable_profiling;
539
540        // Load from each source in order
541        for source in &self.sources {
542            match source {
543                ConfigSource::File { path } => {
544                    if let Ok(content) = fs::read_to_string(path) {
545                        if let Ok(file_config) = toml::from_str::<StreamConfig>(&content) {
546                            new_config = self.merge_configs(new_config, file_config);
547                        }
548                    }
549                }
550                ConfigSource::Environment { prefix } => {
551                    new_config = self.load_from_env(new_config, prefix).await?;
552                }
553                ConfigSource::Memory { data } => {
554                    new_config = self.apply_overrides(new_config, data.clone());
555                }
556                _ => {
557                    // Remote source would be implemented here
558                }
559            }
560        }
561
562        // Apply secrets
563        new_config = self.apply_secrets(new_config).await?;
564
565        // Validate configuration
566        self.validate_config(&new_config)?;
567
568        // Update current configuration
569        *self.current_config.write().await = new_config.clone();
570
571        // Notify listeners
572        let _ = self.change_notifier.send(ConfigChangeEvent::Reloaded {
573            old_config: Box::new(old_config),
574            new_config: Box::new(new_config),
575        });
576
577        info!("Configuration reloaded successfully");
578        Ok(())
579    }
580
581    /// Load configuration from environment variables
582    async fn load_from_env(&self, mut config: StreamConfig, prefix: &str) -> Result<StreamConfig> {
583        // Backend selection
584        if let Ok(backend) = std::env::var(format!("{prefix}_BACKEND")) {
585            config.backend = match backend.as_str() {
586                "kafka" => {
587                    // The Kafka *config selector* stays in oxirs-stream (pure data); the
588                    // rdkafka-backed runtime lives in the publish=false adapter crate.
589                    let brokers: Vec<String> = std::env::var(format!("{prefix}_KAFKA_BROKERS"))
590                        .unwrap_or_else(|_| "localhost:9092".to_string())
591                        .split(',')
592                        .map(|s| s.to_string())
593                        .collect();
594                    StreamBackendType::Kafka {
595                        brokers,
596                        security_protocol: std::env::var(format!("{prefix}_KAFKA_SECURITY")).ok(),
597                        sasl_config: None,
598                    }
599                }
600                "memory" => StreamBackendType::Memory {
601                    max_size: Some(10000),
602                    persistence: false,
603                },
604                _ => config.backend,
605            };
606        }
607
608        // Connection settings
609        if let Ok(max_conn) = std::env::var(format!("{prefix}_MAX_CONNECTIONS")) {
610            if let Ok(val) = max_conn.parse() {
611                config.max_connections = val;
612            }
613        }
614
615        // Compression
616        if let Ok(compression) = std::env::var(format!("{prefix}_COMPRESSION")) {
617            config.compression_type = match compression.as_str() {
618                "gzip" => CompressionType::Gzip,
619                "snappy" => CompressionType::Snappy,
620                "lz4" => CompressionType::Lz4,
621                "zstd" => CompressionType::Zstd,
622                _ => CompressionType::None,
623            };
624            config.enable_compression = compression != "none";
625        }
626
627        Ok(config)
628    }
629
630    /// Apply secrets to configuration
631    async fn apply_secrets(&self, mut config: StreamConfig) -> Result<StreamConfig> {
632        // Apply SASL password if using Kafka
633        if let StreamBackendType::Kafka {
634            brokers,
635            security_protocol,
636            sasl_config: _,
637        } = &config.backend
638        {
639            if security_protocol.as_deref() == Some("SASL_SSL") {
640                if let Ok(username) = self.secret_manager.get_secret("kafka_username").await {
641                    if let Ok(password) = self.secret_manager.get_secret("kafka_password").await {
642                        {
643                            config.backend = StreamBackendType::Kafka {
644                                brokers: brokers.clone(),
645                                security_protocol: security_protocol.clone(),
646                                sasl_config: Some(crate::SaslConfig {
647                                    mechanism: crate::SaslMechanism::ScramSha256,
648                                    username,
649                                    password,
650                                }),
651                            };
652                        }
653                    }
654                }
655            }
656        }
657
658        // Apply TLS certificates
659        if config.security.enable_tls {
660            if let Ok(_cert) = self.tls_manager.get_certificate("client").await {
661                // Certificate paths would be set here
662                config.security.client_cert_path = Some("/tmp/client.crt".to_string());
663                config.security.client_key_path = Some("/tmp/client.key".to_string());
664            }
665        }
666
667        Ok(config)
668    }
669
670    /// Merge two configurations
671    fn merge_configs(&self, _base: StreamConfig, override_config: StreamConfig) -> StreamConfig {
672        // This would implement a proper merge strategy
673        // For now, just return the override
674        override_config
675    }
676
677    /// Apply key-value overrides
678    fn apply_overrides(
679        &self,
680        mut config: StreamConfig,
681        overrides: HashMap<String, String>,
682    ) -> StreamConfig {
683        for (key, value) in overrides {
684            match key.as_str() {
685                "topic" => config.topic = value,
686                "batch_size" => {
687                    if let Ok(size) = value.parse() {
688                        config.batch_size = size;
689                    }
690                }
691                "max_connections" => {
692                    if let Ok(max) = value.parse() {
693                        config.max_connections = max;
694                    }
695                }
696                _ => {}
697            }
698        }
699        config
700    }
701
702    /// Validate configuration
703    fn validate_config(&self, config: &StreamConfig) -> Result<()> {
704        // Validate connection limits
705        if config.max_connections == 0 {
706            return Err(anyhow!("max_connections must be greater than 0"));
707        }
708
709        // Validate batch size
710        if config.batch_size == 0 {
711            return Err(anyhow!("batch_size must be greater than 0"));
712        }
713
714        // Validate topic name
715        if config.topic.is_empty() {
716            return Err(anyhow!("topic name cannot be empty"));
717        }
718
719        // Backend-specific validation
720        match &config.backend {
721            StreamBackendType::Kafka { brokers, .. } if brokers.is_empty() => {
722                return Err(anyhow!("Kafka brokers list cannot be empty"));
723            }
724            _ => {}
725        }
726
727        Ok(())
728    }
729
730    /// Get current configuration
731    pub async fn get_config(&self) -> StreamConfig {
732        self.current_config.read().await.clone()
733    }
734
735    /// Subscribe to configuration changes
736    pub fn subscribe(&self) -> broadcast::Receiver<ConfigChangeEvent> {
737        self.change_notifier.subscribe()
738    }
739
740    /// Apply a performance profile
741    pub async fn apply_performance_profile(&mut self, profile_name: &str) -> Result<()> {
742        let profile = self
743            .performance_profiles
744            .get(profile_name)
745            .ok_or_else(|| anyhow!("Performance profile {} not found", profile_name))?
746            .clone();
747
748        let mut config = self.current_config.write().await;
749        config.performance = profile.settings;
750
751        info!("Applied performance profile: {}", profile_name);
752        Ok(())
753    }
754
755    /// Get available performance profiles
756    pub fn get_performance_profiles(&self) -> Vec<&PerformanceProfile> {
757        self.performance_profiles.values().collect()
758    }
759
760    /// Update a specific configuration value
761    pub async fn update_value(&mut self, key: &str, value: String) -> Result<()> {
762        let mut overrides = HashMap::new();
763        overrides.insert(key.to_string(), value);
764
765        let current = self.current_config.read().await.clone();
766        let updated = self.apply_overrides(current, overrides);
767
768        self.validate_config(&updated)?;
769        *self.current_config.write().await = updated;
770
771        info!("Updated configuration key: {}", key);
772        Ok(())
773    }
774
775    /// Get secret manager
776    pub fn secret_manager(&self) -> &Arc<SecretManager> {
777        &self.secret_manager
778    }
779
780    /// Get TLS manager
781    pub fn tls_manager(&self) -> &Arc<TlsManager> {
782        &self.tls_manager
783    }
784}
785
786/// Configuration builder for easy setup
787pub struct ConfigBuilder {
788    sources: Vec<ConfigSource>,
789    environment: Option<Environment>,
790}
791
792impl ConfigBuilder {
793    /// Create a new configuration builder
794    pub fn new() -> Self {
795        Self {
796            sources: Vec::new(),
797            environment: None,
798        }
799    }
800
801    /// Add a file source
802    pub fn with_file(mut self, path: impl Into<PathBuf>) -> Self {
803        self.sources.push(ConfigSource::File { path: path.into() });
804        self
805    }
806
807    /// Add environment variable source
808    pub fn with_env(mut self, prefix: impl Into<String>) -> Self {
809        self.sources.push(ConfigSource::Environment {
810            prefix: prefix.into(),
811        });
812        self
813    }
814
815    /// Add memory source for overrides
816    pub fn with_overrides(mut self, overrides: HashMap<String, String>) -> Self {
817        self.sources.push(ConfigSource::Memory { data: overrides });
818        self
819    }
820
821    /// Set environment explicitly
822    pub fn with_environment(mut self, env: Environment) -> Self {
823        self.environment = Some(env);
824        self
825    }
826
827    /// Build the configuration manager
828    pub async fn build(self) -> Result<ConfigManager> {
829        ConfigManager::new(self.sources).await
830    }
831}
832
833impl Default for ConfigBuilder {
834    fn default() -> Self {
835        Self::new()
836    }
837}
838
839#[cfg(test)]
840mod tests {
841    use super::*;
842
843    #[tokio::test]
844    async fn test_environment_detection() {
845        let env = Environment::detect();
846        let defaults = env.get_defaults();
847        assert!(defaults.max_connections > 0);
848    }
849
850    #[tokio::test]
851    async fn test_config_builder() {
852        let mut overrides = HashMap::new();
853        overrides.insert("topic".to_string(), "test-topic".to_string());
854
855        let manager = ConfigBuilder::new()
856            .with_env("OXIRS")
857            .with_overrides(overrides)
858            .build()
859            .await
860            .unwrap();
861
862        let config = manager.get_config().await;
863        assert_eq!(config.topic, "test-topic");
864    }
865
866    #[tokio::test]
867    async fn test_secret_manager() {
868        let backend = SecretBackend::Memory {
869            secrets: Arc::new(RwLock::new(HashMap::new())),
870        };
871        let manager = SecretManager::new(backend, Duration::from_secs(60));
872
873        // Set and get secret
874        manager.set_secret("test_key", "test_value").await.unwrap();
875        let value = manager.get_secret("test_key").await.unwrap();
876        assert_eq!(value, "test_value");
877
878        // Test cache
879        let value2 = manager.get_secret("test_key").await.unwrap();
880        assert_eq!(value2, "test_value");
881    }
882
883    #[tokio::test]
884    async fn test_performance_profiles() {
885        let manager = ConfigBuilder::new().build().await.unwrap();
886        let profiles = manager.get_performance_profiles();
887
888        assert!(profiles.len() >= 4);
889        assert!(profiles.iter().any(|p| p.name == "low-latency"));
890        assert!(profiles.iter().any(|p| p.name == "high-throughput"));
891    }
892
893    #[tokio::test]
894    async fn test_config_validation() {
895        let manager = ConfigBuilder::new().build().await.unwrap();
896
897        // Test invalid configuration
898        let invalid_config = StreamConfig {
899            max_connections: 0,
900            ..Default::default()
901        };
902
903        assert!(manager.validate_config(&invalid_config).is_err());
904    }
905}