1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::env;
8use std::fs;
9use std::path::Path;
10
11use crate::errors::{Result, RustisanError};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct Config {
16 pub app_name: String,
18 pub app_env: String,
19 pub app_debug: bool,
20 pub app_url: String,
21 pub app_timezone: String,
22
23 pub server: ServerConfig,
25
26 pub database: DatabaseConfig,
28
29 pub logging: LoggingConfig,
31
32 pub custom: HashMap<String, toml::Value>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct ServerConfig {
39 pub host: String,
40 pub port: u16,
41 pub workers: Option<usize>,
42 pub max_connections: Option<usize>,
43 pub keep_alive: Option<u64>,
44 pub read_timeout: Option<u64>,
45 pub write_timeout: Option<u64>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct DatabaseConfig {
51 pub default: String,
52 pub connections: HashMap<String, DatabaseConnection>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct DatabaseConnection {
58 pub driver: String,
59 pub host: String,
60 pub port: u16,
61 pub database: String,
62 pub username: String,
63 pub password: String,
64 pub charset: Option<String>,
65 pub prefix: Option<String>,
66 pub pool_size: Option<u32>,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct LoggingConfig {
72 pub level: String,
73 pub format: String,
74 pub output: String,
75 pub file_path: Option<String>,
76 pub max_file_size: Option<u64>,
77 pub max_files: Option<u32>,
78}
79
80impl Config {
81 pub fn load() -> Result<Self> {
83 let mut config = Self::default();
85
86 if Path::new("rustisan.toml").exists() {
88 config = Self::load_from_file("rustisan.toml")?;
89 }
90
91 config.load_from_env();
93
94 Ok(config)
95 }
96
97 pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
99 let content = fs::read_to_string(path.as_ref()).map_err(|e| {
100 RustisanError::ConfigError(format!(
101 "Failed to read config file {}: {}",
102 path.as_ref().display(),
103 e
104 ))
105 })?;
106
107 let config: Config = toml::from_str(&content).map_err(|e| {
108 RustisanError::ConfigError(format!("Failed to parse config file: {}", e))
109 })?;
110
111 Ok(config)
112 }
113
114 pub fn load_from_env(&mut self) {
116 if let Ok(name) = env::var("APP_NAME") {
118 self.app_name = name;
119 }
120 if let Ok(env_val) = env::var("APP_ENV") {
121 self.app_env = env_val;
122 }
123 if let Ok(debug) = env::var("APP_DEBUG") {
124 self.app_debug = debug.parse().unwrap_or(false);
125 }
126 if let Ok(url) = env::var("APP_URL") {
127 self.app_url = url;
128 }
129 if let Ok(timezone) = env::var("APP_TIMEZONE") {
130 self.app_timezone = timezone;
131 }
132
133 if let Ok(host) = env::var("SERVER_HOST") {
135 self.server.host = host;
136 }
137 if let Ok(port) = env::var("SERVER_PORT") {
138 if let Ok(port_num) = port.parse() {
139 self.server.port = port_num;
140 }
141 }
142 if let Ok(workers) = env::var("SERVER_WORKERS") {
143 if let Ok(workers_num) = workers.parse() {
144 self.server.workers = Some(workers_num);
145 }
146 }
147
148 if let Ok(db_url) = env::var("DATABASE_URL") {
150 if let Ok(connection) = Self::parse_database_url(&db_url) {
152 self.database.connections.insert("default".to_string(), connection);
153 }
154 }
155
156 if let Ok(log_level) = env::var("LOG_LEVEL") {
158 self.logging.level = log_level;
159 }
160 }
161
162 fn parse_database_url(url: &str) -> Result<DatabaseConnection> {
164 let parts: Vec<&str> = url.splitn(2, "://").collect();
168 if parts.len() != 2 {
169 return Err(RustisanError::ConfigError("Invalid database URL format".to_string()));
170 }
171
172 let driver = parts[0].to_string();
173 let rest = parts[1];
174
175 let (credentials, host_db) = if let Some(at_pos) = rest.find('@') {
177 let (creds, host_db) = rest.split_at(at_pos);
178 (creds, &host_db[1..]) } else {
180 ("", rest)
181 };
182
183 let (username, password) = if credentials.contains(':') {
184 let cred_parts: Vec<&str> = credentials.splitn(2, ':').collect();
185 (cred_parts[0].to_string(), cred_parts[1].to_string())
186 } else {
187 (credentials.to_string(), String::new())
188 };
189
190 let (host_port, database) = if let Some(slash_pos) = host_db.find('/') {
191 let (hp, db) = host_db.split_at(slash_pos);
192 (hp, &db[1..]) } else {
194 (host_db, "")
195 };
196
197 let (host, port) = if let Some(colon_pos) = host_port.find(':') {
198 let (h, p) = host_port.split_at(colon_pos);
199 let port_num = p[1..].parse().unwrap_or(5432); (h.to_string(), port_num)
201 } else {
202 (host_port.to_string(), 5432)
203 };
204
205 Ok(DatabaseConnection {
206 driver,
207 host,
208 port,
209 database: database.to_string(),
210 username,
211 password,
212 charset: Some("utf8".to_string()),
213 prefix: None,
214 pool_size: Some(10),
215 })
216 }
217
218 pub fn database_connection(&self, name: &str) -> Option<&DatabaseConnection> {
220 self.database.connections.get(name)
221 }
222
223 pub fn default_database_connection(&self) -> Option<&DatabaseConnection> {
225 self.database_connection(&self.database.default)
226 }
227
228 pub fn get<T>(&self, key: &str) -> Option<T>
230 where
231 T: for<'a> Deserialize<'a>,
232 {
233 self.custom.get(key).and_then(|value| {
234 T::deserialize(value.clone()).ok()
235 })
236 }
237
238 pub fn set<T>(&mut self, key: String, value: T)
240 where
241 T: Serialize,
242 {
243 if let Ok(toml_value) = toml::Value::try_from(value) {
244 self.custom.insert(key, toml_value);
245 }
246 }
247
248 pub fn is_debug(&self) -> bool {
250 self.app_debug
251 }
252
253 pub fn is_production(&self) -> bool {
255 self.app_env == "production"
256 }
257
258 pub fn is_development(&self) -> bool {
260 self.app_env == "development" || self.app_env == "dev"
261 }
262
263 pub fn server_address(&self) -> String {
265 format!("{}:{}", self.server.host, self.server.port)
266 }
267}
268
269impl Default for Config {
270 fn default() -> Self {
271 let mut database_connections = HashMap::new();
272 database_connections.insert(
273 "default".to_string(),
274 DatabaseConnection {
275 driver: "sqlite".to_string(),
276 host: "localhost".to_string(),
277 port: 0,
278 database: "database.sqlite".to_string(),
279 username: String::new(),
280 password: String::new(),
281 charset: Some("utf8".to_string()),
282 prefix: None,
283 pool_size: Some(10),
284 },
285 );
286
287 Self {
288 app_name: "Rustisan Application".to_string(),
289 app_env: "development".to_string(),
290 app_debug: true,
291 app_url: "http://localhost:3000".to_string(),
292 app_timezone: "UTC".to_string(),
293
294 server: ServerConfig {
295 host: "127.0.0.1".to_string(),
296 port: 3000,
297 workers: None,
298 max_connections: Some(1000),
299 keep_alive: Some(75),
300 read_timeout: Some(30),
301 write_timeout: Some(30),
302 },
303
304 database: DatabaseConfig {
305 default: "default".to_string(),
306 connections: database_connections,
307 },
308
309 logging: LoggingConfig {
310 level: "info".to_string(),
311 format: "json".to_string(),
312 output: "stdout".to_string(),
313 file_path: None,
314 max_file_size: Some(10 * 1024 * 1024), max_files: Some(5),
316 },
317
318 custom: HashMap::new(),
319 }
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 #[test]
328 fn test_default_config() {
329 let config = Config::default();
330 assert_eq!(config.app_name, "Rustisan Application");
331 assert_eq!(config.server.host, "127.0.0.1");
332 assert_eq!(config.server.port, 3000);
333 assert!(config.is_development());
334 assert!(!config.is_production());
335 }
336
337 #[test]
338 fn test_database_url_parsing() {
339 let url = "postgresql://user:pass@localhost:5432/mydb";
340 let connection = Config::parse_database_url(url).unwrap();
341
342 assert_eq!(connection.driver, "postgresql");
343 assert_eq!(connection.username, "user");
344 assert_eq!(connection.password, "pass");
345 assert_eq!(connection.host, "localhost");
346 assert_eq!(connection.port, 5432);
347 assert_eq!(connection.database, "mydb");
348 }
349
350 #[test]
351 fn test_server_address() {
352 let config = Config::default();
353 assert_eq!(config.server_address(), "127.0.0.1:3000");
354 }
355
356 #[test]
357 fn test_custom_config() {
358 let mut config = Config::default();
359 config.set("custom_key".to_string(), "custom_value");
360
361 let value: Option<String> = config.get("custom_key");
362 assert_eq!(value, Some("custom_value".to_string()));
363 }
364
365 #[test]
366 fn test_environment_modes() {
367 let mut config = Config::default();
368
369 config.app_env = "production".to_string();
370 assert!(config.is_production());
371 assert!(!config.is_development());
372
373 config.app_env = "development".to_string();
374 assert!(!config.is_production());
375 assert!(config.is_development());
376 }
377}