Skip to main content

allsource_core/infrastructure/config/
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    /// Also supports standard PORT env var for serverless platforms (Cloud Run, Fly.io, etc.)
259    pub fn from_env() -> Result<Self> {
260        let mut config = Config::default();
261
262        // Server - check ALLSOURCE_HOST first, then HOST (serverless fallback)
263        if let Ok(host) = std::env::var("ALLSOURCE_HOST").or_else(|_| std::env::var("HOST")) {
264            config.server.host = host;
265        }
266
267        // Port priority: ALLSOURCE_PORT > PORT (serverless standard)
268        let port_str = std::env::var("ALLSOURCE_PORT").or_else(|_| std::env::var("PORT"));
269        if let Ok(port) = port_str {
270            config.server.port = port
271                .parse()
272                .map_err(|_| AllSourceError::ValidationError("Invalid port number".to_string()))?;
273        }
274
275        // Storage
276        if let Ok(data_dir) = std::env::var("ALLSOURCE_DATA_DIR") {
277            config.storage.data_dir = PathBuf::from(data_dir);
278        }
279
280        // Auth
281        if let Ok(jwt_secret) = std::env::var("ALLSOURCE_JWT_SECRET") {
282            config.auth.jwt_secret = jwt_secret;
283        }
284
285        Ok(config)
286    }
287
288    /// Load configuration with fallback priority:
289    /// 1. Config file (if provided)
290    /// 2. Environment variables
291    /// 3. Defaults
292    pub fn load(config_path: Option<PathBuf>) -> Result<Self> {
293        let mut config = if let Some(path) = config_path {
294            if path.exists() {
295                tracing::info!("Loading config from: {}", path.display());
296                Self::from_file(path)?
297            } else {
298                tracing::warn!("Config file not found: {}, using defaults", path.display());
299                Config::default()
300            }
301        } else {
302            Config::default()
303        };
304
305        // Override with environment variables
306        if let Ok(env_config) = Self::from_env() {
307            config.merge_env(env_config);
308        }
309
310        config.validate()?;
311
312        Ok(config)
313    }
314
315    /// Merge environment variable overrides
316    fn merge_env(&mut self, env_config: Config) {
317        // Merge server config
318        if env_config.server.host != ServerConfig::default().host {
319            self.server.host = env_config.server.host;
320        }
321        if env_config.server.port != ServerConfig::default().port {
322            self.server.port = env_config.server.port;
323        }
324
325        // Merge storage config
326        if env_config.storage.data_dir != StorageConfig::default().data_dir {
327            self.storage.data_dir = env_config.storage.data_dir;
328        }
329
330        // Merge auth config
331        if env_config.auth.jwt_secret != AuthConfig::default().jwt_secret {
332            self.auth.jwt_secret = env_config.auth.jwt_secret;
333        }
334    }
335
336    /// Validate configuration
337    pub fn validate(&self) -> Result<()> {
338        // Validate port
339        if self.server.port == 0 {
340            return Err(AllSourceError::ValidationError(
341                "Server port cannot be 0".to_string(),
342            ));
343        }
344
345        // Validate JWT secret in production
346        if self.auth.jwt_secret == "CHANGE_ME_IN_PRODUCTION" {
347            tracing::warn!("⚠️  Using default JWT secret - INSECURE for production!");
348        }
349
350        // Validate storage paths
351        if self.storage.data_dir.as_os_str().is_empty() {
352            return Err(AllSourceError::ValidationError(
353                "Data directory path cannot be empty".to_string(),
354            ));
355        }
356
357        Ok(())
358    }
359
360    /// Save configuration to TOML file
361    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
362        let toml = toml::to_string_pretty(self).map_err(|e| {
363            AllSourceError::ValidationError(format!("Failed to serialize config: {}", e))
364        })?;
365
366        fs::write(path.as_ref(), toml).map_err(|e| {
367            AllSourceError::StorageError(format!("Failed to write config file: {}", e))
368        })?;
369
370        Ok(())
371    }
372
373    /// Generate example configuration file
374    pub fn example() -> String {
375        toml::to_string_pretty(&Config::default())
376            .unwrap_or_else(|_| String::from("# Failed to generate example config"))
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383
384    #[test]
385    fn test_default_config() {
386        let config = Config::default();
387        assert_eq!(config.server.port, 3900);
388        assert!(config.rate_limit.enabled);
389    }
390
391    #[test]
392    fn test_config_validation() {
393        let config = Config::default();
394        assert!(config.validate().is_ok());
395    }
396
397    #[test]
398    fn test_invalid_port() {
399        let mut config = Config::default();
400        config.server.port = 0;
401        assert!(config.validate().is_err());
402    }
403
404    #[test]
405    fn test_config_serialization() {
406        let config = Config::default();
407        let toml = toml::to_string(&config).unwrap();
408        let deserialized: Config = toml::from_str(&toml).unwrap();
409        assert_eq!(config.server.port, deserialized.server.port);
410    }
411}