1use serde::{Deserialize, Serialize};
11use std::fs;
12use std::path::{Path, PathBuf};
13use crate::error::{AllSourceError, Result};
14
15#[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#[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, max_connections: 10_000,
60 request_timeout_secs: 30,
61 cors_enabled: true,
62 cors_origins: vec!["*".to_string()],
63 }
64 }
65}
66
67#[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#[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#[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#[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, retention_count: 7,
171 compression_level: 6,
172 verify_after_backup: true,
173 }
174 }
175}
176
177#[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#[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 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 pub fn from_env() -> Result<Self> {
259 let mut config = Config::default();
260
261 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 if let Ok(data_dir) = std::env::var("ALLSOURCE_DATA_DIR") {
272 config.storage.data_dir = PathBuf::from(data_dir);
273 }
274
275 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 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 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 fn merge_env(&mut self, env_config: Config) {
312 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 if env_config.storage.data_dir != StorageConfig::default().data_dir {
322 self.storage.data_dir = env_config.storage.data_dir;
323 }
324
325 if env_config.auth.jwt_secret != AuthConfig::default().jwt_secret {
327 self.auth.jwt_secret = env_config.auth.jwt_secret;
328 }
329 }
330
331 pub fn validate(&self) -> Result<()> {
333 if self.server.port == 0 {
335 return Err(AllSourceError::ValidationError(
336 "Server port cannot be 0".to_string(),
337 ));
338 }
339
340 if self.auth.jwt_secret == "CHANGE_ME_IN_PRODUCTION" {
342 tracing::warn!("⚠️ Using default JWT secret - INSECURE for production!");
343 }
344
345 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 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 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}