1use crate::error::{AllSourceError, Result};
2use serde::{Deserialize, Serialize};
11use std::fs;
12use std::path::{Path, PathBuf};
13
14#[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#[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, max_connections: 10_000,
59 request_timeout_secs: 30,
60 cors_enabled: true,
61 cors_origins: vec!["*".to_string()],
62 }
63 }
64}
65
66#[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#[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#[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#[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, retention_count: 7,
170 compression_level: 6,
171 verify_after_backup: true,
172 }
173 }
174}
175
176#[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#[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 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 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
267 .parse()
268 .map_err(|_| AllSourceError::ValidationError("Invalid port number".to_string()))?;
269 }
270
271 if let Ok(data_dir) = std::env::var("ALLSOURCE_DATA_DIR") {
273 config.storage.data_dir = PathBuf::from(data_dir);
274 }
275
276 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 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 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 fn merge_env(&mut self, env_config: Config) {
313 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 if env_config.storage.data_dir != StorageConfig::default().data_dir {
323 self.storage.data_dir = env_config.storage.data_dir;
324 }
325
326 if env_config.auth.jwt_secret != AuthConfig::default().jwt_secret {
328 self.auth.jwt_secret = env_config.auth.jwt_secret;
329 }
330 }
331
332 pub fn validate(&self) -> Result<()> {
334 if self.server.port == 0 {
336 return Err(AllSourceError::ValidationError(
337 "Server port cannot be 0".to_string(),
338 ));
339 }
340
341 if self.auth.jwt_secret == "CHANGE_ME_IN_PRODUCTION" {
343 tracing::warn!("⚠️ Using default JWT secret - INSECURE for production!");
344 }
345
346 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 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 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}