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
114fn 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 }
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 pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
222 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 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 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 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}