1use crate::error::{CoreError, CoreResult, ErrorContext};
7use std::collections::HashMap;
8use std::env;
9use std::fmt;
10use std::fs;
11use std::path::Path;
12use std::str::FromStr;
13use std::sync::RwLock;
14use std::time::SystemTime;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum ConfigSource {
19 Environment,
21 File(String),
23 CommandLine,
25 Default,
27 Override,
29}
30
31#[derive(Debug, Clone)]
33pub struct ConfigValue {
34 pub value: String,
36 pub source: ConfigSource,
38 pub timestamp: SystemTime,
40 pub is_sensitive: bool,
42 pub description: Option<String>,
44}
45
46#[derive(Debug, Clone)]
48pub struct ConfigEntry {
49 pub key: String,
51 pub value: ConfigValue,
53 pub validator: Option<String>,
55 pub hot_reloadable: bool,
57 pub defaultvalue: Option<String>,
59 pub env_var: Option<String>,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Hash)]
65pub enum Environment {
66 Development,
68 Testing,
70 Staging,
72 Production,
74 Custom(String),
76}
77
78impl fmt::Display for Environment {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 match self {
81 Environment::Development => write!(f, "development"),
82 Environment::Testing => write!(f, "testing"),
83 Environment::Staging => write!(f, "staging"),
84 Environment::Production => write!(f, "production"),
85 Environment::Custom(name) => write!(f, "{name}"),
86 }
87 }
88}
89
90impl std::str::FromStr for Environment {
91 type Err = std::convert::Infallible;
92
93 fn from_str(s: &str) -> Result<Self, Self::Err> {
95 let env = match s.to_lowercase().as_str() {
96 "dev" | "development" => Environment::Development,
97 "test" | "testing" => Environment::Testing,
98 "stage" | "staging" => Environment::Staging,
99 "prod" | "production" => Environment::Production,
100 name => Environment::Custom(name.to_string()),
101 };
102 Ok(env)
103 }
104}
105
106#[derive(Debug, Clone)]
108pub struct FeatureFlag {
109 pub name: String,
111 pub enabled: bool,
113 pub rollout_percentage: f64,
115 pub environments: Vec<Environment>,
117 pub user_groups: Vec<String>,
119 pub description: Option<String>,
121 pub last_modified: SystemTime,
123}
124
125#[derive(Debug, Clone)]
127pub struct ValidationResult {
128 pub is_valid: bool,
130 pub errors: Vec<String>,
132 pub warnings: Vec<String>,
134}
135
136pub trait ConfigValidator: Send + Sync {
138 fn validate(&self, value: &str) -> ValidationResult;
140
141 fn name(&self) -> &str;
143
144 fn help(&self) -> &str;
146}
147
148pub struct PositiveIntValidator;
151
152impl ConfigValidator for PositiveIntValidator {
153 fn validate(&self, value: &str) -> ValidationResult {
154 match value.parse::<i64>() {
155 Ok(n) if n > 0 => ValidationResult {
156 is_valid: true,
157 errors: Vec::new(),
158 warnings: Vec::new(),
159 },
160 Ok(n) => ValidationResult {
161 is_valid: false,
162 errors: vec![format!("Value must be positive, got {n}")],
163 warnings: Vec::new(),
164 },
165 Err(_) => ValidationResult {
166 is_valid: false,
167 errors: vec![format!("Invalid integer: {value}")],
168 warnings: Vec::new(),
169 },
170 }
171 }
172
173 fn name(&self) -> &str {
174 "positive_int"
175 }
176
177 fn help(&self) -> &str {
178 "Must be a positive integer greater than 0"
179 }
180}
181
182pub struct PortValidator;
184
185impl ConfigValidator for PortValidator {
186 fn validate(&self, value: &str) -> ValidationResult {
187 match value.parse::<u16>() {
188 Ok(0) => ValidationResult {
189 is_valid: false,
190 errors: vec!["Port 0 is not allowed".to_string()],
191 warnings: Vec::new(),
192 },
193 Ok(port) => ValidationResult {
194 is_valid: true,
195 errors: Vec::new(),
196 warnings: if port < 1024 {
197 vec!["Port number is below 1024 (requires root privileges)".to_string()]
198 } else {
199 Vec::new()
200 },
201 },
202 Err(_) => ValidationResult {
203 is_valid: false,
204 errors: vec![format!("Invalid port number: {value}")],
205 warnings: Vec::new(),
206 },
207 }
208 }
209
210 fn name(&self) -> &str {
211 "port"
212 }
213
214 fn help(&self) -> &str {
215 "Must be a valid port number (1-65535)"
216 }
217}
218
219pub struct UrlValidator;
221
222impl ConfigValidator for UrlValidator {
223 fn validate(&self, value: &str) -> ValidationResult {
224 if value.starts_with("http://") || value.starts_with("https://") {
226 ValidationResult {
227 is_valid: true,
228 errors: Vec::new(),
229 warnings: if value.starts_with("http://") {
230 vec!["Using HTTP instead of HTTPS may be insecure".to_string()]
231 } else {
232 Vec::new()
233 },
234 }
235 } else {
236 ValidationResult {
237 is_valid: false,
238 errors: vec![format!("Invalid URL format: {value}")],
239 warnings: Vec::new(),
240 }
241 }
242 }
243
244 fn name(&self) -> &str {
245 "url"
246 }
247
248 fn help(&self) -> &str {
249 "Must be a valid HTTP or HTTPS URL"
250 }
251}
252
253pub struct ProductionConfig {
255 entries: RwLock<HashMap<String, ConfigEntry>>,
257 feature_flags: RwLock<HashMap<String, FeatureFlag>>,
259 validators: HashMap<String, Box<dyn ConfigValidator>>,
261 environment: Environment,
263 file_watchers: RwLock<HashMap<String, SystemTime>>,
265 hot_reload_enabled: bool,
267}
268
269impl ProductionConfig {
270 pub fn register_config(&mut self, key: &str, value: String) -> CoreResult<()> {
272 self.set(key, value.as_str())
274 }
275
276 pub fn new() -> Self {
278 let mut validators: HashMap<String, Box<dyn ConfigValidator>> = HashMap::new();
279 validators.insert("positive_int".to_string(), Box::new(PositiveIntValidator));
280 validators.insert("port".to_string(), Box::new(PortValidator));
281 validators.insert("url".to_string(), Box::new(UrlValidator));
282
283 let env_str = env::var("SCIRS_ENV").unwrap_or_else(|_| "development".to_string());
285 let environment = Environment::from_str(&env_str).unwrap_or(Environment::Development);
286
287 Self {
288 entries: RwLock::new(HashMap::new()),
289 feature_flags: RwLock::new(HashMap::new()),
290 validators,
291 environment,
292 file_watchers: RwLock::new(HashMap::new()),
293 hot_reload_enabled: true,
294 }
295 }
296
297 pub fn load_from_env(&self) -> CoreResult<()> {
299 let mut entries = self.entries.write().map_err(|_| {
300 CoreError::ConfigError(ErrorContext::new("Failed to acquire config lock"))
301 })?;
302
303 for (key, value) in env::vars() {
305 if key.starts_with("SCIRS_") {
306 let config_key = key.strip_prefix("SCIRS_").unwrap().to_lowercase();
307
308 let config_value = ConfigValue {
309 value: value.clone(),
310 source: ConfigSource::Environment,
311 timestamp: SystemTime::now(),
312 is_sensitive: self.is_sensitive_key(&config_key),
313 description: None,
314 };
315
316 let entry = ConfigEntry {
317 key: config_key.clone(),
318 value: config_value,
319 validator: None,
320 hot_reloadable: false, defaultvalue: None,
322 env_var: Some(key),
323 };
324
325 entries.insert(config_key, entry);
326 }
327 }
328
329 Ok(())
330 }
331
332 pub fn load_from_file<P: AsRef<Path>>(&self, path: P) -> CoreResult<()> {
334 let path = path.as_ref();
335 let content = fs::read_to_string(path).map_err(|e| {
336 CoreError::ConfigError(ErrorContext::new(format!(
337 "Failed to read config file {path}: {e}",
338 path = path.display()
339 )))
340 })?;
341
342 if let Ok(metadata) = fs::metadata(path) {
344 if let Ok(modified) = metadata.modified() {
345 if let Ok(mut watchers) = self.file_watchers.write() {
346 watchers.insert(path.to_string_lossy().to_string(), modified);
347 }
348 }
349 }
350
351 match path.extension().and_then(|ext| ext.to_str()) {
353 Some("json") => self.parse_json_config(&content),
354 Some("yaml" | "yml") => self.parse_yaml_config(&content),
355 Some("toml") => self.parse_toml_config(&content),
356 _ => Err(CoreError::ConfigError(ErrorContext::new(format!(
357 "Unsupported config file format: {path}",
358 path = path.display()
359 )))),
360 }
361 }
362
363 fn parse_json_config(&self, content: &str) -> CoreResult<()> {
365 Err(CoreError::ConfigError(ErrorContext::new(
367 "JSON parsing not implemented in this example",
368 )))
369 }
370
371 fn parse_yaml_config(&self, content: &str) -> CoreResult<()> {
373 Err(CoreError::ConfigError(ErrorContext::new(
375 "YAML parsing not implemented in this example",
376 )))
377 }
378
379 fn parse_toml_config(&self, content: &str) -> CoreResult<()> {
381 Err(CoreError::ConfigError(ErrorContext::new(
383 "TOML parsing not implemented in this example",
384 )))
385 }
386
387 pub fn set<S: Into<String>>(&self, key: S, value: S) -> CoreResult<()> {
389 let key = key.into();
390 let value = value.into();
391
392 if let Some(entry) = self.get_entry(&key)? {
394 if let Some(validator_name) = &entry.validator {
395 if let Some(validator) = self.validators.get(validator_name) {
396 let validation_result = validator.validate(&value);
397 if !validation_result.is_valid {
398 return Err(CoreError::ConfigError(ErrorContext::new(format!(
399 "Validation failed for {key}: {errors}",
400 errors = validation_result.errors.join(", ")
401 ))));
402 }
403 }
404 }
405 }
406
407 let mut entries = self.entries.write().map_err(|_| {
408 CoreError::ConfigError(ErrorContext::new("Failed to acquire config lock"))
409 })?;
410
411 let config_value = ConfigValue {
412 value,
413 source: ConfigSource::Override,
414 timestamp: SystemTime::now(),
415 is_sensitive: self.is_sensitive_key(&key),
416 description: None,
417 };
418
419 if let Some(entry) = entries.get_mut(&key) {
420 entry.value = config_value;
421 } else {
422 let entry = ConfigEntry {
423 key: key.clone(),
424 value: config_value,
425 validator: None,
426 hot_reloadable: true,
427 defaultvalue: None,
428 env_var: None,
429 };
430 entries.insert(key, entry);
431 }
432
433 Ok(())
434 }
435
436 pub fn get(&self, key: &str) -> CoreResult<Option<String>> {
438 let entries = self.entries.read().map_err(|_| {
439 CoreError::ConfigError(ErrorContext::new("Failed to acquire config lock"))
440 })?;
441
442 Ok(entries.get(key).map(|entry| entry.value.value.clone()))
443 }
444
445 pub fn get_entry(&self, key: &str) -> CoreResult<Option<ConfigEntry>> {
447 let entries = self.entries.read().map_err(|_| {
448 CoreError::ConfigError(ErrorContext::new("Failed to acquire config lock"))
449 })?;
450
451 Ok(entries.get(key).cloned())
452 }
453
454 pub fn get_typed<T>(&self, key: &str) -> CoreResult<Option<T>>
456 where
457 T: std::str::FromStr,
458 T::Err: fmt::Display,
459 {
460 if let Some(value_str) = self.get(key)? {
461 match value_str.parse::<T>() {
462 Ok(value) => Ok(Some(value)),
463 Err(e) => Err(CoreError::ConfigError(ErrorContext::new(format!(
464 "Failed to parse config value '{value_str}' for key '{key}': {e}"
465 )))),
466 }
467 } else {
468 Ok(None)
469 }
470 }
471
472 pub fn get_or_default<T>(&self, key: &str, default: T) -> CoreResult<T>
474 where
475 T: std::str::FromStr + Clone,
476 T::Err: fmt::Display,
477 {
478 match self.get_typed::<T>(key)? {
479 Some(value) => Ok(value),
480 None => Ok(default),
481 }
482 }
483
484 pub fn register(
486 &mut self,
487 key: String,
488 defaultvalue: Option<String>,
489 validator: Option<String>,
490 reloadable: bool,
491 description: Option<String>,
492 ) -> CoreResult<()> {
493 let mut entries = self.entries.write().map_err(|_| {
494 CoreError::ConfigError(ErrorContext::new("Failed to acquire config lock"))
495 })?;
496
497 if entries.contains_key(&key) {
499 return Ok(()); }
501
502 let value = ConfigValue {
503 value: defaultvalue.clone().unwrap_or_default(),
504 source: ConfigSource::Default,
505 timestamp: SystemTime::now(),
506 is_sensitive: self.is_sensitive_key(&key),
507 description: description.clone(),
508 };
509
510 let entry = ConfigEntry {
511 key: key.clone(),
512 value,
513 validator,
514 hot_reloadable: reloadable,
515 defaultvalue,
516 env_var: Some(format!("SCIRS_{}", key.to_uppercase())),
517 };
518
519 entries.insert(key, entry);
520 Ok(())
521 }
522
523 fn is_sensitive_key(&self, key: &str) -> bool {
525 let sensitive_patterns = ["password", "secret", "key", "token", "credential", "auth"];
526 let key_lower = key.to_lowercase();
527 sensitive_patterns
528 .iter()
529 .any(|pattern| key_lower.contains(pattern))
530 }
531
532 pub const fn environment(&self) -> &Environment {
534 &self.environment
535 }
536
537 pub fn set_feature_flag(
539 &mut self,
540 name: String,
541 enabled: bool,
542 rollout_percentage: f64,
543 ) -> CoreResult<()> {
544 let mut flags = self.feature_flags.write().map_err(|_| {
545 CoreError::ConfigError(ErrorContext::new("Failed to acquire feature flags lock"))
546 })?;
547
548 let flag = FeatureFlag {
549 name: name.clone(),
550 enabled,
551 rollout_percentage: rollout_percentage.clamp(0.0, 100.0),
552 environments: vec![self.environment.clone()],
553 user_groups: Vec::new(),
554 description: None,
555 last_modified: SystemTime::now(),
556 };
557
558 flags.insert(name, flag);
559 Ok(())
560 }
561
562 pub fn is_feature_enabled(&self, name: &str) -> bool {
564 if let Ok(flags) = self.feature_flags.read() {
565 if let Some(flag) = flags.get(name) {
566 return flag.enabled
567 && flag.environments.contains(&self.environment)
568 && self.check_rollout_percentage(flag.rollout_percentage);
569 }
570 }
571 false
572 }
573
574 fn check_rollout_percentage(&self, percentage: f64) -> bool {
576 use std::collections::hash_map::DefaultHasher;
579 use std::hash::{Hash, Hasher};
580
581 let mut hasher = DefaultHasher::new();
582 SystemTime::now().hash(&mut hasher);
583 let hash = hasher.finish();
584
585 (hash % 100) < percentage as u64
586 }
587
588 pub fn validate_all(&self) -> CoreResult<HashMap<String, ValidationResult>> {
590 let entries = self.entries.read().map_err(|_| {
591 CoreError::ConfigError(ErrorContext::new("Failed to acquire config lock"))
592 })?;
593
594 let mut results = HashMap::new();
595
596 for (key, entry) in entries.iter() {
597 if let Some(validator_name) = &entry.validator {
598 if let Some(validator) = self.validators.get(validator_name) {
599 let result = validator.validate(&entry.value.value);
600 results.insert(key.clone(), result);
601 }
602 }
603 }
604
605 Ok(results)
606 }
607
608 pub fn get_summary(&self) -> CoreResult<ConfigSummary> {
610 let entries = self.entries.read().map_err(|_| {
611 CoreError::ConfigError(ErrorContext::new("Failed to acquire config lock"))
612 })?;
613
614 let flags = self.feature_flags.read().map_err(|_| {
615 CoreError::ConfigError(ErrorContext::new("Failed to acquire feature flags lock"))
616 })?;
617
618 let total_configs = entries.len();
619 let env_configs = entries
620 .values()
621 .filter(|e| e.value.source == ConfigSource::Environment)
622 .count();
623 let file_configs = entries
624 .values()
625 .filter(|e| matches!(e.value.source, ConfigSource::File(_)))
626 .count();
627 let override_configs = entries
628 .values()
629 .filter(|e| e.value.source == ConfigSource::Override)
630 .count();
631 let sensitive_configs = entries.values().filter(|e| e.value.is_sensitive).count();
632
633 let total_flags = flags.len();
634 let enabled_flags = flags.values().filter(|f| f.enabled).count();
635
636 Ok(ConfigSummary {
637 environment: self.environment.clone(),
638 total_configs,
639 env_configs,
640 file_configs,
641 override_configs,
642 sensitive_configs,
643 total_flags,
644 enabled_flags,
645 hot_reload_enabled: self.hot_reload_enabled,
646 })
647 }
648
649 pub fn reload(&self) -> CoreResult<Vec<String>> {
651 if !self.hot_reload_enabled {
652 return Err(CoreError::ConfigError(ErrorContext::new(
653 "Hot reload is disabled",
654 )));
655 }
656
657 let mut reloaded_files = Vec::new();
658
659 if let Ok(watchers) = self.file_watchers.read() {
660 for (file_path, last_modified) in watchers.iter() {
661 if let Ok(metadata) = fs::metadata(file_path) {
662 if let Ok(current_modified) = metadata.modified() {
663 if current_modified > *last_modified {
664 if let Err(e) = self.load_from_file(file_path) {
666 eprintln!("Failed to reload config file {file_path}: {e}");
667 } else {
668 reloaded_files.push(file_path.clone());
669 }
670 }
671 }
672 }
673 }
674 }
675
676 Ok(reloaded_files)
677 }
678}
679
680impl Default for ProductionConfig {
681 fn default() -> Self {
682 Self::new()
683 }
684}
685
686#[derive(Debug, Clone)]
688pub struct ConfigSummary {
689 pub environment: Environment,
690 pub total_configs: usize,
691 pub env_configs: usize,
692 pub file_configs: usize,
693 pub override_configs: usize,
694 pub sensitive_configs: usize,
695 pub total_flags: usize,
696 pub enabled_flags: usize,
697 pub hot_reload_enabled: bool,
698}
699
700impl fmt::Display for ConfigSummary {
701 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
702 writeln!(f, "Configuration Summary:")?;
703 writeln!(f, " Environment: {}", self.environment)?;
704 writeln!(f, " Total configurations: {}", self.total_configs)?;
705 writeln!(f, " From environment: {}", self.env_configs)?;
706 writeln!(f, " From files: {}", self.file_configs)?;
707 writeln!(f, " Runtime overrides: {}", self.override_configs)?;
708 writeln!(f, " Sensitive configs: {}", self.sensitive_configs)?;
709 writeln!(f, " Feature flags:")?;
710 writeln!(f, " Total: {}", self.total_flags)?;
711 writeln!(f, " Enabled: {}", self.enabled_flags)?;
712 writeln!(
713 f,
714 " Hot reload: {}",
715 if self.hot_reload_enabled {
716 "enabled"
717 } else {
718 "disabled"
719 }
720 )?;
721 Ok(())
722 }
723}
724
725static GLOBAL_CONFIG: std::sync::LazyLock<ProductionConfig> = std::sync::LazyLock::new(|| {
727 let mut config = ProductionConfig::new();
728
729 if let Err(e) = config.load_from_env() {
731 eprintln!("Warning: Failed to load configuration from environment: {e}");
732 }
733
734 let _ = config.register(
736 "log_level".to_string(),
737 Some("info".to_string()),
738 None,
739 true,
740 Some("Logging level (trace, debug, info, warn, error)".to_string()),
741 );
742
743 let _ = config.register(
744 "max_memory_mb".to_string(),
745 Some("1024".to_string()),
746 Some("positive_int".to_string()),
747 true,
748 Some("Maximum memory usage in megabytes".to_string()),
749 );
750
751 let _ = config.register(
752 "worker_threads".to_string(),
753 Some("4".to_string()),
754 Some("positive_int".to_string()),
755 false,
756 Some("Number of worker threads".to_string()),
757 );
758
759 config
760});
761
762#[allow(dead_code)]
764pub fn global_config() -> &'static ProductionConfig {
765 &GLOBAL_CONFIG
766}
767
768#[macro_export]
771macro_rules! config_get {
772 ($key:expr) => {
773 $crate::config::production::global_config().get($key)
774 };
775 ($key:expr, $type:ty) => {
776 $crate::config::production::global_config().get_typed::<$type>($key)
777 };
778 ($key:expr, $type:ty, $default:expr) => {
779 $crate::config::production::global_config().get_or_default::<$type>($key, $default)
780 };
781}
782
783#[macro_export]
785macro_rules! config_set {
786 ($key:expr, $value:expr) => {
787 $crate::config::production::global_config().set($key, $value)
788 };
789}
790
791#[macro_export]
793macro_rules! feature_enabled {
794 ($feature:expr) => {
795 $crate::config::production::global_config().is_feature_enabled($feature)
796 };
797}
798
799#[cfg(test)]
800mod tests {
801 use super::*;
802
803 #[test]
804 fn test_environment_parsing() {
805 assert_eq!(
806 Environment::from_str("development"),
807 Ok(Environment::Development)
808 );
809 assert_eq!(Environment::from_str("dev"), Ok(Environment::Development));
810 assert_eq!(
811 Environment::from_str("production"),
812 Ok(Environment::Production)
813 );
814 assert_eq!(
815 Environment::from_str("custom"),
816 Ok(Environment::Custom("custom".to_string()))
817 );
818 }
819
820 #[test]
821 fn test_validators() {
822 let validator = PositiveIntValidator;
823
824 let result = validator.validate("42");
825 assert!(result.is_valid);
826
827 let result = validator.validate("-5");
828 assert!(!result.is_valid);
829
830 let result = validator.validate("not_a_number");
831 assert!(!result.is_valid);
832 }
833
834 #[test]
835 fn test_config_operations() {
836 let config = ProductionConfig::new();
837
838 config.set("test_key", "test_value").unwrap();
840 assert_eq!(
841 config.get("test_key").unwrap(),
842 Some("test_value".to_string())
843 );
844
845 config.set("test_number", "42").unwrap();
847 assert_eq!(config.get_typed::<i32>("test_number").unwrap(), Some(42));
848
849 assert_eq!(config.get_or_default("missing_key", 100i32).unwrap(), 100);
851 }
852
853 #[test]
854 fn test_feature_flags() {
855 let mut config = ProductionConfig::new();
856
857 config
858 .set_feature_flag("test_feature".to_string(), true, 100.0)
859 .unwrap();
860 assert!(config.is_feature_enabled("test_feature"));
861
862 config
863 .set_feature_flag("disabled_feature".to_string(), false, 100.0)
864 .unwrap();
865 assert!(!config.is_feature_enabled("disabled_feature"));
866 }
867
868 #[test]
869 fn test_sensitive_key_detection() {
870 let config = ProductionConfig::new();
871
872 assert!(config.is_sensitive_key("api_password"));
873 assert!(config.is_sensitive_key("SECRET_KEY"));
874 assert!(config.is_sensitive_key("auth_token"));
875 assert!(!config.is_sensitive_key("log_level"));
876 assert!(!config.is_sensitive_key("max_connections"));
877 }
878
879 #[test]
880 fn test_config_registration() {
881 let mut config = ProductionConfig::new();
882
883 config
884 .register(
885 "test_config".to_string(),
886 Some("defaultvalue".to_string()),
887 Some("positive_int".to_string()),
888 true,
889 Some("Test configuration".to_string()),
890 )
891 .unwrap();
892
893 let entry = config.get_entry("test_config").unwrap().unwrap();
894 assert_eq!(entry.defaultvalue, Some("defaultvalue".to_string()));
895 assert_eq!(entry.validator, Some("positive_int".to_string()));
896 assert!(entry.hot_reloadable);
897 }
898}