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