allsource_core/
config.rs

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