oxidite_config/
lib.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use std::env;
4use std::fs;
5use std::path::Path;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum Environment {
9    Development,
10    Testing,
11    Production,
12}
13
14impl Environment {
15    pub fn from_str(s: &str) -> Self {
16        match s.to_lowercase().as_str() {
17            "production" | "prod" => Self::Production,
18            "testing" | "test" => Self::Testing,
19            _ => Self::Development,
20        }
21    }
22
23    pub fn as_str(&self) -> &str {
24        match self {
25            Self::Development => "development",
26            Self::Testing => "testing",
27            Self::Production => "production",
28        }
29    }
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct Config {
34    #[serde(default)]
35    pub app: AppConfig,
36    #[serde(default)]
37    pub server: ServerConfig,
38    #[serde(default)]
39    pub database: DatabaseConfig,
40    #[serde(default)]
41    pub cache: CacheConfig,
42    #[serde(default)]
43    pub queue: QueueConfig,
44    #[serde(default)]
45    pub security: SecurityConfig,
46    #[serde(default)]
47    pub custom: HashMap<String, toml::Value>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct AppConfig {
52    #[serde(default = "default_app_name")]
53    pub name: String,
54    #[serde(default)]
55    pub version: String,
56    #[serde(default)]
57    pub environment: String,
58    #[serde(default)]
59    pub debug: bool,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct ServerConfig {
64    #[serde(default = "default_host")]
65    pub host: String,
66    #[serde(default = "default_port")]
67    pub port: u16,
68    #[serde(default)]
69    pub workers: usize,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct DatabaseConfig {
74    #[serde(default)]
75    pub url: String,
76    #[serde(default = "default_pool_size")]
77    pub pool_size: u32,
78    #[serde(default)]
79    pub ssl: bool,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct CacheConfig {
84    #[serde(default)]
85    pub driver: String,
86    #[serde(default)]
87    pub redis_url: String,
88    #[serde(default = "default_ttl")]
89    pub default_ttl: u64,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct QueueConfig {
94    #[serde(default)]
95    pub driver: String,
96    #[serde(default)]
97    pub redis_url: String,
98    #[serde(default = "default_workers")]
99    pub workers: usize,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct SecurityConfig {
104    #[serde(default)]
105    pub jwt_secret: String,
106    #[serde(default = "default_jwt_expiry")]
107    pub jwt_expiry: u64,
108    #[serde(default)]
109    pub cors_origins: Vec<String>,
110    #[serde(default)]
111    pub rate_limit: u32,
112}
113
114// Default functions
115fn default_app_name() -> String {
116    "oxidite-app".to_string()
117}
118
119fn default_host() -> String {
120    "127.0.0.1".to_string()
121}
122
123fn default_port() -> u16 {
124    3000
125}
126
127fn default_pool_size() -> u32 {
128    10
129}
130
131fn default_ttl() -> u64 {
132    3600
133}
134
135fn default_workers() -> usize {
136    4
137}
138
139fn default_jwt_expiry() -> u64 {
140    900 // 15 minutes
141}
142
143impl Default for AppConfig {
144    fn default() -> Self {
145        Self {
146            name: default_app_name(),
147            version: env!("CARGO_PKG_VERSION").to_string(),
148            environment: "development".to_string(),
149            debug: true,
150        }
151    }
152}
153
154impl Default for ServerConfig {
155    fn default() -> Self {
156        Self {
157            host: default_host(),
158            port: default_port(),
159            workers: num_cpus::get(),
160        }
161    }
162}
163
164impl Default for DatabaseConfig {
165    fn default() -> Self {
166        Self {
167            url: String::new(),
168            pool_size: default_pool_size(),
169            ssl: false,
170        }
171    }
172}
173
174impl Default for CacheConfig {
175    fn default() -> Self {
176        Self {
177            driver: "memory".to_string(),
178            redis_url: String::new(),
179            default_ttl: default_ttl(),
180        }
181    }
182}
183
184impl Default for QueueConfig {
185    fn default() -> Self {
186        Self {
187            driver: "memory".to_string(),
188            redis_url: String::new(),
189            workers: default_workers(),
190        }
191    }
192}
193
194impl Default for SecurityConfig {
195    fn default() -> Self {
196        Self {
197            jwt_secret: String::new(),
198            jwt_expiry: default_jwt_expiry(),
199            cors_origins: vec![],
200            rate_limit: 0,
201        }
202    }
203}
204
205impl Default for Config {
206    fn default() -> Self {
207        Self {
208            app: AppConfig::default(),
209            server: ServerConfig::default(),
210            database: DatabaseConfig::default(),
211            cache: CacheConfig::default(),
212            queue: QueueConfig::default(),
213            security: SecurityConfig::default(),
214            custom: HashMap::new(),
215        }
216    }
217}
218
219impl Config {
220    /// Load configuration from environment variables and config files
221    pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
222        // Load .env file if it exists
223        let _ = dotenv::dotenv();
224
225        let env = env::var("OXIDITE_ENV")
226            .or_else(|_| env::var("ENVIRONMENT"))
227            .unwrap_or_else(|_| "development".to_string());
228
229        // Try to load oxidite.toml
230        let mut config = if Path::new("oxidite.toml").exists() {
231            let content = fs::read_to_string("oxidite.toml")?;
232            toml::from_str(&content)?
233        } else {
234            Config::default()
235        };
236
237        // Override with environment variables
238        if let Ok(val) = env::var("APP_NAME") {
239            config.app.name = val;
240        }
241        if let Ok(val) = env::var("SERVER_HOST") {
242            config.server.host = val;
243        }
244        if let Ok(val) = env::var("SERVER_PORT") {
245            config.server.port = val.parse().unwrap_or(default_port());
246        }
247        if let Ok(val) = env::var("DATABASE_URL") {
248            config.database.url = val;
249        }
250        if let Ok(val) = env::var("REDIS_URL") {
251            config.cache.redis_url = val.clone();
252            config.queue.redis_url = val;
253        }
254        if let Ok(val) = env::var("JWT_SECRET") {
255            config.security.jwt_secret = val;
256        }
257
258        config.app.environment = env;
259
260        Ok(config)
261    }
262
263    /// Get value from custom configuration
264    pub fn get<T: for<'de> Deserialize<'de>>(&self, key: &str) -> Option<T> {
265        self.custom.get(key).and_then(|v| T::deserialize(v.clone()).ok())
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[test]
274    fn test_default_config() {
275        let config = Config::default();
276        assert_eq!(config.server.host, "127.0.0.1");
277        assert_eq!(config.server.port, 3000);
278    }
279
280    #[test]
281    fn test_environment_parsing() {
282        assert_eq!(Environment::from_str("production"), Environment::Production);
283        assert_eq!(Environment::from_str("PROD"), Environment::Production);
284        assert_eq!(Environment::from_str("development"), Environment::Development);
285    }
286}