rustisan_core/
config.rs

1//! Configuration module for the Rustisan framework
2//!
3//! This module provides configuration management similar to Laravel's config system.
4
5use 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/// Main application configuration
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct Config {
16    /// Application settings
17    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    /// Server settings
24    pub server: ServerConfig,
25
26    /// Database settings
27    pub database: DatabaseConfig,
28
29    /// Logging settings
30    pub logging: LoggingConfig,
31
32    /// Custom settings
33    pub custom: HashMap<String, toml::Value>,
34}
35
36/// Server configuration
37#[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/// Database configuration
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct DatabaseConfig {
51    pub default: String,
52    pub connections: HashMap<String, DatabaseConnection>,
53}
54
55/// Individual database connection configuration
56#[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/// Logging configuration
70#[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    /// Loads configuration from environment variables and config files
82    pub fn load() -> Result<Self> {
83        // Start with default configuration
84        let mut config = Self::default();
85
86        // Try to load from rustisan.toml if it exists
87        if Path::new("rustisan.toml").exists() {
88            config = Self::load_from_file("rustisan.toml")?;
89        }
90
91        // Override with environment variables
92        config.load_from_env();
93
94        Ok(config)
95    }
96
97    /// Loads configuration from a TOML file
98    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    /// Loads configuration from environment variables
115    pub fn load_from_env(&mut self) {
116        // App settings
117        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        // Server settings
134        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        // Database settings
149        if let Ok(db_url) = env::var("DATABASE_URL") {
150            // Parse DATABASE_URL and update default connection
151            if let Ok(connection) = Self::parse_database_url(&db_url) {
152                self.database.connections.insert("default".to_string(), connection);
153            }
154        }
155
156        // Logging settings
157        if let Ok(log_level) = env::var("LOG_LEVEL") {
158            self.logging.level = log_level;
159        }
160    }
161
162    /// Parses a database URL into a DatabaseConnection
163    fn parse_database_url(url: &str) -> Result<DatabaseConnection> {
164        // Simple parsing for common database URLs
165        // Format: driver://username:password@host:port/database
166
167        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        // Parse the rest
176        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..]) // Remove the '@'
179        } 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..]) // Remove the '/'
193        } 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); // Default PostgreSQL port
200            (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    /// Gets a database connection configuration
219    pub fn database_connection(&self, name: &str) -> Option<&DatabaseConnection> {
220        self.database.connections.get(name)
221    }
222
223    /// Gets the default database connection
224    pub fn default_database_connection(&self) -> Option<&DatabaseConnection> {
225        self.database_connection(&self.database.default)
226    }
227
228    /// Gets a custom configuration value
229    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    /// Sets a custom configuration value
239    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    /// Checks if the application is in debug mode
249    pub fn is_debug(&self) -> bool {
250        self.app_debug
251    }
252
253    /// Checks if the application is in production mode
254    pub fn is_production(&self) -> bool {
255        self.app_env == "production"
256    }
257
258    /// Checks if the application is in development mode
259    pub fn is_development(&self) -> bool {
260        self.app_env == "development" || self.app_env == "dev"
261    }
262
263    /// Gets the server bind address
264    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), // 10MB
315                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}