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
54#[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#[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#[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#[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, retention_count: 7,
158 compression_level: 6,
159 verify_after_backup: true,
160 }
161 }
162}
163
164#[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#[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 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 pub fn from_env() -> Result<Self> {
248 let mut config = Config::default();
249
250 if let Ok(host) = std::env::var("ALLSOURCE_HOST").or_else(|_| std::env::var("HOST")) {
252 config.server.host = host;
253 }
254
255 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 if let Ok(data_dir) = std::env::var("ALLSOURCE_DATA_DIR") {
265 config.storage.data_dir = PathBuf::from(data_dir);
266 }
267
268 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 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 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 fn merge_env(&mut self, env_config: Config) {
305 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 if env_config.storage.data_dir != StorageConfig::default().data_dir {
315 self.storage.data_dir = env_config.storage.data_dir;
316 }
317
318 if env_config.auth.jwt_secret != AuthConfig::default().jwt_secret {
320 self.auth.jwt_secret = env_config.auth.jwt_secret;
321 }
322 }
323
324 pub fn validate(&self) -> Result<()> {
326 if self.server.port == 0 {
328 return Err(AllSourceError::ValidationError(
329 "Server port cannot be 0".to_string(),
330 ));
331 }
332
333 if self.auth.jwt_secret == "CHANGE_ME_IN_PRODUCTION" {
335 tracing::warn!("⚠️ Using default JWT secret - INSECURE for production!");
336 }
337
338 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 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 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}