Skip to main content

backbone_core/config/
mod.rs

1//! Configuration management system for Backbone Framework
2//!
3//! Provides a comprehensive, type-safe configuration system with:
4//! - YAML/TOML/JSON file loading
5//! - Environment variable substitution (`${VAR:default}`)
6//! - Validation with detailed error messages
7//! - Default implementations for all config sections
8//!
9//! # Example
10//!
11//! ```ignore
12//! use backbone_core::config::BackboneConfig;
13//!
14//! // Load from file with environment overrides
15//! let config = BackboneConfig::from_file("config/application.yml")?;
16//!
17//! // Or load from environment only
18//! let config = BackboneConfig::from_env()?;
19//!
20//! // Access configuration
21//! println!("Server port: {}", config.server.port);
22//! println!("Database URL: {}", config.database.default().url);
23//! ```
24
25mod error;
26mod loader;
27mod schema;
28mod bus;
29
30// Domain-specific configuration modules (split from types.rs for maintainability)
31mod app_config;
32mod contexts_config;
33mod database_config;
34mod features_config;
35mod logging_config;
36mod modules_config;
37mod monitoring_config;
38mod security_config;
39mod server_config;
40
41pub use error::{ConfigError, ConfigResult};
42pub use loader::ConfigLoader;
43pub use schema::{ConfigValidationError, validate_config};
44
45// Re-export all configuration types
46pub use app_config::{AppConfig, Environment};
47pub use contexts_config::{ContextsConfig, RedisEventBusConfig};
48pub use database_config::{CacheConfig, DatabaseConfig};
49pub use features_config::{FeaturesConfig, RateLimitingConfig};
50pub use logging_config::{LoggingConfig, LoggingFileConfig};
51pub use modules_config::{
52    ModulesConfig, PasswordHasherConfig, PostmanConfig, SapiensAuthConfig, SapiensConfig,
53    SapiensLockoutConfig, SmtpConfig, StorageConfig, BucketConfig, TemplatesConfig,
54};
55pub use monitoring_config::MonitoringConfig;
56pub use security_config::{CsrfConfig, SecurityConfig, SecurityHeadersConfig};
57pub use server_config::ServerConfig;
58
59// Configuration Bus for cross-module configuration sharing
60pub use bus::{ConfigurationBus, ConfigValue, ConfigChangeEvent};
61
62use serde::{Deserialize, Serialize};
63use std::collections::HashMap;
64use std::path::Path;
65
66/// Main configuration struct for Backbone Framework
67///
68/// Contains all configuration sections needed to run a Backbone application.
69/// Supports loading from YAML, TOML, or JSON files with environment variable
70/// substitution.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct BackboneConfig {
73    /// Application metadata
74    pub app: AppConfig,
75    /// Server configuration
76    pub server: ServerConfig,
77    /// Database configurations (keyed by name)
78    #[serde(default)]
79    pub database: HashMap<String, DatabaseConfig>,
80    /// Cache configurations (keyed by name)
81    #[serde(default)]
82    pub cache: HashMap<String, CacheConfig>,
83    /// Module configurations
84    pub modules: ModulesConfig,
85    /// Logging configuration
86    pub logging: LoggingConfig,
87    /// Monitoring configuration
88    pub monitoring: MonitoringConfig,
89    /// Cross-context communication
90    pub contexts: ContextsConfig,
91    /// Feature flags
92    pub features: FeaturesConfig,
93    /// Security settings
94    pub security: SecurityConfig,
95}
96
97impl BackboneConfig {
98    /// Load configuration from a file path
99    ///
100    /// Supports YAML (.yml, .yaml), TOML (.toml), and JSON (.json) formats.
101    /// Environment variables in the format `${VAR:default}` are substituted.
102    ///
103    /// # Example
104    ///
105    /// ```ignore
106    /// let config = BackboneConfig::from_file("config/application.yml")?;
107    /// ```
108    pub fn from_file<P: AsRef<Path>>(path: P) -> ConfigResult<Self> {
109        ConfigLoader::load_file(path)
110    }
111
112    /// Load configuration with environment-specific overrides
113    ///
114    /// Loads base config from `{base_path}` and merges with
115    /// `{base_path}-{env}.{ext}` if it exists.
116    ///
117    /// # Example
118    ///
119    /// ```ignore
120    /// // Loads config/application.yml + config/application-development.yml
121    /// let config = BackboneConfig::from_file_with_env(
122    ///     "config/application.yml",
123    ///     "development"
124    /// )?;
125    /// ```
126    pub fn from_file_with_env<P: AsRef<Path>>(base_path: P, env: &str) -> ConfigResult<Self> {
127        ConfigLoader::load_with_env(base_path, env)
128    }
129
130    /// Load configuration from environment variables only
131    ///
132    /// Uses default values and overrides with environment variables.
133    pub fn from_env() -> ConfigResult<Self> {
134        let mut config = Self::default();
135        config.apply_env_overrides()?;
136        config.validate()?;
137        Ok(config)
138    }
139
140    /// Apply environment variable overrides to configuration
141    fn apply_env_overrides(&mut self) -> ConfigResult<()> {
142        // Server overrides
143        if let Ok(host) = std::env::var("HOST") {
144            self.server.host = host;
145        }
146        if let Ok(port) = std::env::var("PORT") {
147            self.server.port = port.parse().map_err(|_| {
148                ConfigError::env_var("PORT")
149            })?;
150        }
151        if let Ok(workers) = std::env::var("WORKERS") {
152            self.server.workers = Some(workers.parse().map_err(|_| {
153                ConfigError::env_var("WORKERS")
154            })?);
155        }
156
157        // Database overrides
158        if let Ok(url) = std::env::var("DATABASE_URL") {
159            if let Some(db) = self.database.get_mut("default") {
160                db.url = url;
161            }
162        }
163
164        // JWT secret override
165        if let Ok(secret) = std::env::var("JWT_SECRET") {
166            if let Some(ref mut auth) = self.modules.sapiens.auth {
167                auth.jwt_secret = secret;
168            }
169        }
170
171        // Redis override
172        if let Ok(url) = std::env::var("REDIS_URL") {
173            if let Some(cache) = self.cache.get_mut("default") {
174                cache.url = url;
175            }
176        }
177
178        Ok(())
179    }
180
181    /// Validate the configuration
182    ///
183    /// Returns an error if any critical configuration is invalid.
184    pub fn validate(&self) -> ConfigResult<()> {
185        validate_config(self)
186    }
187
188    /// Get the default database configuration
189    pub fn default_database(&self) -> Option<&DatabaseConfig> {
190        self.database.get("default")
191    }
192
193    /// Get a database configuration by name
194    pub fn get_database(&self, name: &str) -> Option<&DatabaseConfig> {
195        self.database.get(name)
196    }
197
198    /// Get the default cache configuration
199    pub fn default_cache(&self) -> Option<&CacheConfig> {
200        self.cache.get("default")
201    }
202
203    /// Get a cache configuration by name
204    pub fn get_cache(&self, name: &str) -> Option<&CacheConfig> {
205        self.cache.get(name)
206    }
207
208    /// Check if a module is enabled
209    pub fn is_module_enabled(&self, module_name: &str) -> bool {
210        match module_name {
211            "sapiens" => self.modules.sapiens.enabled,
212            "postman" => self.modules.postman.enabled,
213            "bucket" => self.modules.bucket.enabled,
214            _ => false,
215        }
216    }
217
218    /// Get the current environment
219    pub fn environment(&self) -> &Environment {
220        &self.app.environment
221    }
222
223    /// Check if running in production
224    pub fn is_production(&self) -> bool {
225        matches!(self.app.environment, Environment::Production)
226    }
227
228    /// Check if running in development
229    pub fn is_development(&self) -> bool {
230        matches!(self.app.environment, Environment::Development)
231    }
232
233    /// Check if debug mode is enabled
234    pub fn is_debug(&self) -> bool {
235        self.app.debug
236    }
237
238    /// Merge another configuration into this one
239    ///
240    /// Values from `other` override values in `self`.
241    pub fn merge(mut self, other: Self) -> Self {
242        // Merge app config
243        if other.app.name != self.app.name {
244            self.app = other.app;
245        }
246
247        // Merge server config
248        self.server = other.server;
249
250        // Merge databases (add/override)
251        for (name, db_config) in other.database {
252            self.database.insert(name, db_config);
253        }
254
255        // Merge caches (add/override)
256        for (name, cache_config) in other.cache {
257            self.cache.insert(name, cache_config);
258        }
259
260        // Merge modules
261        self.modules = other.modules;
262
263        // Merge logging
264        self.logging = other.logging;
265
266        // Merge monitoring
267        self.monitoring = other.monitoring;
268
269        // Merge contexts
270        self.contexts = other.contexts;
271
272        // Merge features
273        self.features = other.features;
274
275        // Merge security
276        self.security = other.security;
277
278        self
279    }
280}
281
282impl Default for BackboneConfig {
283    fn default() -> Self {
284        let mut database = HashMap::new();
285        database.insert(
286            "default".to_string(),
287            DatabaseConfig::default(),
288        );
289
290        let mut cache = HashMap::new();
291        cache.insert(
292            "default".to_string(),
293            CacheConfig::default(),
294        );
295
296        Self {
297            app: AppConfig::default(),
298            server: ServerConfig::default(),
299            database,
300            cache,
301            modules: ModulesConfig::default(),
302            logging: LoggingConfig::default(),
303            monitoring: MonitoringConfig::default(),
304            contexts: ContextsConfig::default(),
305            features: FeaturesConfig::default(),
306            security: SecurityConfig::default(),
307        }
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    #[test]
316    fn test_default_config() {
317        let config = BackboneConfig::default();
318
319        assert_eq!(config.server.host, "0.0.0.0");
320        assert_eq!(config.server.port, 3000);
321        assert!(config.default_database().is_some());
322        assert!(config.default_cache().is_some());
323    }
324
325    #[test]
326    fn test_module_enabled() {
327        let config = BackboneConfig::default();
328
329        assert!(config.is_module_enabled("sapiens"));
330        assert!(!config.is_module_enabled("postman"));
331        assert!(!config.is_module_enabled("bucket"));
332        assert!(!config.is_module_enabled("unknown"));
333    }
334
335    #[test]
336    fn test_environment_checks() {
337        let mut config = BackboneConfig::default();
338
339        assert!(config.is_development());
340        assert!(!config.is_production());
341
342        config.app.environment = Environment::Production;
343        assert!(config.is_production());
344        assert!(!config.is_development());
345    }
346
347    #[test]
348    fn test_merge_configs() {
349        let mut config1 = BackboneConfig::default();
350        config1.server.port = 3000;
351
352        let mut config2 = BackboneConfig::default();
353        config2.server.port = 8080;
354        config2.server.host = "127.0.0.1".to_string();
355
356        let merged = config1.merge(config2);
357
358        assert_eq!(merged.server.port, 8080);
359        assert_eq!(merged.server.host, "127.0.0.1");
360    }
361}