1use crate::error::{AllSourceError, Result};
2use serde::{Deserialize, Serialize};
11use std::{
12 fs,
13 path::{Path, PathBuf},
14};
15
16#[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#[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, 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 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#[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#[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#[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#[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, retention_count: 7,
178 compression_level: 6,
179 verify_after_backup: true,
180 }
181 }
182}
183
184#[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#[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 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 pub fn from_env() -> Result<Self> {
268 let mut config = Config::default();
269
270 if let Ok(host) = std::env::var("ALLSOURCE_HOST").or_else(|_| std::env::var("HOST")) {
272 config.server.host = host;
273 }
274
275 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 if let Ok(data_dir) = std::env::var("ALLSOURCE_DATA_DIR") {
285 config.storage.data_dir = PathBuf::from(data_dir);
286 }
287
288 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 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 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 fn merge_env(&mut self, env_config: Config) {
325 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 if env_config.storage.data_dir != StorageConfig::default().data_dir {
335 self.storage.data_dir = env_config.storage.data_dir;
336 }
337
338 if env_config.auth.jwt_secret != AuthConfig::default().jwt_secret {
340 self.auth.jwt_secret = env_config.auth.jwt_secret;
341 }
342 }
343
344 pub fn validate(&self) -> Result<()> {
346 if self.server.port == 0 {
348 return Err(AllSourceError::ValidationError(
349 "Server port cannot be 0".to_string(),
350 ));
351 }
352
353 if self.auth.jwt_secret == "CHANGE_ME_IN_PRODUCTION" {
355 tracing::warn!("⚠️ Using default JWT secret - INSECURE for production!");
356 }
357
358 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 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 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}