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