allsource_core/
config.rs

1/// Configuration management for AllSource v1.0
2///
3/// Features:
4/// - Environment-based configuration
5/// - TOML file support
6/// - Runtime configuration validation
7/// - Hot-reloading support (via file watcher)
8/// - Secure credential handling
9
10use serde::{Deserialize, Serialize};
11use std::fs;
12use std::path::{Path, PathBuf};
13use crate::error::{AllSourceError, Result};
14
15/// Main application configuration
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct Config {
18    pub server: ServerConfig,
19    pub storage: StorageConfig,
20    pub auth: AuthConfig,
21    pub rate_limit: RateLimitConfigFile,
22    pub backup: BackupConfigFile,
23    pub metrics: MetricsConfig,
24    pub logging: LoggingConfig,
25}
26
27impl Default for Config {
28    fn default() -> Self {
29        Self {
30            server: ServerConfig::default(),
31            storage: StorageConfig::default(),
32            auth: AuthConfig::default(),
33            rate_limit: RateLimitConfigFile::default(),
34            backup: BackupConfigFile::default(),
35            metrics: MetricsConfig::default(),
36            logging: LoggingConfig::default(),
37        }
38    }
39}
40
41/// Server configuration
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ServerConfig {
44    pub host: String,
45    pub port: u16,
46    pub workers: Option<usize>,
47    pub max_connections: usize,
48    pub request_timeout_secs: u64,
49    pub cors_enabled: bool,
50    pub cors_origins: Vec<String>,
51}
52
53impl Default for ServerConfig {
54    fn default() -> Self {
55        Self {
56            host: "0.0.0.0".to_string(),
57            port: 3900,
58            workers: None, // Use number of CPUs
59            max_connections: 10_000,
60            request_timeout_secs: 30,
61            cors_enabled: true,
62            cors_origins: vec!["*".to_string()],
63        }
64    }
65}
66
67/// Storage configuration
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct StorageConfig {
70    pub data_dir: PathBuf,
71    pub wal_dir: PathBuf,
72    pub batch_size: usize,
73    pub compression: CompressionType,
74    pub retention_days: Option<u32>,
75    pub max_storage_gb: Option<u32>,
76}
77
78impl Default for StorageConfig {
79    fn default() -> Self {
80        Self {
81            data_dir: PathBuf::from("./data"),
82            wal_dir: PathBuf::from("./wal"),
83            batch_size: 1000,
84            compression: CompressionType::Lz4,
85            retention_days: None,
86            max_storage_gb: None,
87        }
88    }
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
92#[serde(rename_all = "lowercase")]
93pub enum CompressionType {
94    None,
95    Lz4,
96    Gzip,
97    Snappy,
98}
99
100/// Authentication configuration
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct AuthConfig {
103    pub jwt_secret: String,
104    pub jwt_expiry_hours: i64,
105    pub api_key_expiry_days: Option<i64>,
106    pub password_min_length: usize,
107    pub require_email_verification: bool,
108    pub session_timeout_minutes: u64,
109}
110
111impl Default for AuthConfig {
112    fn default() -> Self {
113        Self {
114            jwt_secret: "CHANGE_ME_IN_PRODUCTION".to_string(),
115            jwt_expiry_hours: 24,
116            api_key_expiry_days: Some(90),
117            password_min_length: 8,
118            require_email_verification: false,
119            session_timeout_minutes: 60,
120        }
121    }
122}
123
124/// Rate limiting configuration
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct RateLimitConfigFile {
127    pub enabled: bool,
128    pub default_tier: RateLimitTier,
129    pub requests_per_minute: Option<u32>,
130    pub burst_size: Option<u32>,
131}
132
133impl Default for RateLimitConfigFile {
134    fn default() -> Self {
135        Self {
136            enabled: true,
137            default_tier: RateLimitTier::Professional,
138            requests_per_minute: None,
139            burst_size: None,
140        }
141    }
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
145#[serde(rename_all = "lowercase")]
146pub enum RateLimitTier {
147    Free,
148    Professional,
149    Unlimited,
150    Custom,
151}
152
153/// Backup configuration
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct BackupConfigFile {
156    pub enabled: bool,
157    pub backup_dir: PathBuf,
158    pub schedule_cron: Option<String>,
159    pub retention_count: usize,
160    pub compression_level: u8,
161    pub verify_after_backup: bool,
162}
163
164impl Default for BackupConfigFile {
165    fn default() -> Self {
166        Self {
167            enabled: false,
168            backup_dir: PathBuf::from("./backups"),
169            schedule_cron: None, // e.g., "0 2 * * *" for 2am daily
170            retention_count: 7,
171            compression_level: 6,
172            verify_after_backup: true,
173        }
174    }
175}
176
177/// Metrics configuration
178#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct MetricsConfig {
180    pub enabled: bool,
181    pub endpoint: String,
182    pub push_interval_secs: Option<u64>,
183    pub push_gateway_url: Option<String>,
184}
185
186impl Default for MetricsConfig {
187    fn default() -> Self {
188        Self {
189            enabled: true,
190            endpoint: "/metrics".to_string(),
191            push_interval_secs: None,
192            push_gateway_url: None,
193        }
194    }
195}
196
197/// Logging configuration
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct LoggingConfig {
200    pub level: LogLevel,
201    pub format: LogFormat,
202    pub output: LogOutput,
203    pub file_path: Option<PathBuf>,
204    pub rotate_size_mb: Option<u64>,
205}
206
207impl Default for LoggingConfig {
208    fn default() -> Self {
209        Self {
210            level: LogLevel::Info,
211            format: LogFormat::Pretty,
212            output: LogOutput::Stdout,
213            file_path: None,
214            rotate_size_mb: Some(100),
215        }
216    }
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
220#[serde(rename_all = "lowercase")]
221pub enum LogLevel {
222    Trace,
223    Debug,
224    Info,
225    Warn,
226    Error,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
230#[serde(rename_all = "lowercase")]
231pub enum LogFormat {
232    Json,
233    Pretty,
234    Compact,
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
238#[serde(rename_all = "lowercase")]
239pub enum LogOutput {
240    Stdout,
241    Stderr,
242    File,
243    Both,
244}
245
246impl Config {
247    /// Load configuration from file
248    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
249        let content = fs::read_to_string(path.as_ref())
250            .map_err(|e| AllSourceError::StorageError(format!("Failed to read config file: {}", e)))?;
251
252        toml::from_str(&content)
253            .map_err(|e| AllSourceError::ValidationError(format!("Invalid config format: {}", e)))
254    }
255
256    /// Load configuration from environment variables
257    /// Variables are prefixed with ALLSOURCE_
258    pub fn from_env() -> Result<Self> {
259        let mut config = Config::default();
260
261        // Server
262        if let Ok(host) = std::env::var("ALLSOURCE_HOST") {
263            config.server.host = host;
264        }
265        if let Ok(port) = std::env::var("ALLSOURCE_PORT") {
266            config.server.port = port.parse()
267                .map_err(|_| AllSourceError::ValidationError("Invalid port number".to_string()))?;
268        }
269
270        // Storage
271        if let Ok(data_dir) = std::env::var("ALLSOURCE_DATA_DIR") {
272            config.storage.data_dir = PathBuf::from(data_dir);
273        }
274
275        // Auth
276        if let Ok(jwt_secret) = std::env::var("ALLSOURCE_JWT_SECRET") {
277            config.auth.jwt_secret = jwt_secret;
278        }
279
280        Ok(config)
281    }
282
283    /// Load configuration with fallback priority:
284    /// 1. Config file (if provided)
285    /// 2. Environment variables
286    /// 3. Defaults
287    pub fn load(config_path: Option<PathBuf>) -> Result<Self> {
288        let mut config = if let Some(path) = config_path {
289            if path.exists() {
290                tracing::info!("Loading config from: {}", path.display());
291                Self::from_file(path)?
292            } else {
293                tracing::warn!("Config file not found: {}, using defaults", path.display());
294                Config::default()
295            }
296        } else {
297            Config::default()
298        };
299
300        // Override with environment variables
301        if let Ok(env_config) = Self::from_env() {
302            config.merge_env(env_config);
303        }
304
305        config.validate()?;
306
307        Ok(config)
308    }
309
310    /// Merge environment variable overrides
311    fn merge_env(&mut self, env_config: Config) {
312        // Merge server config
313        if env_config.server.host != ServerConfig::default().host {
314            self.server.host = env_config.server.host;
315        }
316        if env_config.server.port != ServerConfig::default().port {
317            self.server.port = env_config.server.port;
318        }
319
320        // Merge storage config
321        if env_config.storage.data_dir != StorageConfig::default().data_dir {
322            self.storage.data_dir = env_config.storage.data_dir;
323        }
324
325        // Merge auth config
326        if env_config.auth.jwt_secret != AuthConfig::default().jwt_secret {
327            self.auth.jwt_secret = env_config.auth.jwt_secret;
328        }
329    }
330
331    /// Validate configuration
332    pub fn validate(&self) -> Result<()> {
333        // Validate port
334        if self.server.port == 0 {
335            return Err(AllSourceError::ValidationError(
336                "Server port cannot be 0".to_string(),
337            ));
338        }
339
340        // Validate JWT secret in production
341        if self.auth.jwt_secret == "CHANGE_ME_IN_PRODUCTION" {
342            tracing::warn!("⚠️  Using default JWT secret - INSECURE for production!");
343        }
344
345        // Validate storage paths
346        if self.storage.data_dir.as_os_str().is_empty() {
347            return Err(AllSourceError::ValidationError(
348                "Data directory path cannot be empty".to_string(),
349            ));
350        }
351
352        Ok(())
353    }
354
355    /// Save configuration to TOML file
356    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
357        let toml = toml::to_string_pretty(self)
358            .map_err(|e| AllSourceError::ValidationError(format!("Failed to serialize config: {}", e)))?;
359
360        fs::write(path.as_ref(), toml)
361            .map_err(|e| AllSourceError::StorageError(format!("Failed to write config file: {}", e)))?;
362
363        Ok(())
364    }
365
366    /// Generate example configuration file
367    pub fn example() -> String {
368        toml::to_string_pretty(&Config::default()).unwrap_or_else(|_| String::from("# Failed to generate example config"))
369    }
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375
376    #[test]
377    fn test_default_config() {
378        let config = Config::default();
379        assert_eq!(config.server.port, 8080);
380        assert!(config.rate_limit.enabled);
381    }
382
383    #[test]
384    fn test_config_validation() {
385        let config = Config::default();
386        assert!(config.validate().is_ok());
387    }
388
389    #[test]
390    fn test_invalid_port() {
391        let mut config = Config::default();
392        config.server.port = 0;
393        assert!(config.validate().is_err());
394    }
395
396    #[test]
397    fn test_config_serialization() {
398        let config = Config::default();
399        let toml = toml::to_string(&config).unwrap();
400        let deserialized: Config = toml::from_str(&toml).unwrap();
401        assert_eq!(config.server.port, deserialized.server.port);
402    }
403}