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
307 .strip_prefix("SCIRS_")
308 .expect("Operation failed")
309 .to_lowercase();
310
311 let config_value = ConfigValue {
312 value: value.clone(),
313 source: ConfigSource::Environment,
314 timestamp: SystemTime::now(),
315 is_sensitive: self.is_sensitive_key(&config_key),
316 description: None,
317 };
318
319 let entry = ConfigEntry {
320 key: config_key.clone(),
321 value: config_value,
322 validator: None,
323 hot_reloadable: false, defaultvalue: None,
325 env_var: Some(key),
326 };
327
328 entries.insert(config_key, entry);
329 }
330 }
331
332 Ok(())
333 }
334
335 pub fn load_from_file<P: AsRef<Path>>(&self, path: P) -> CoreResult<()> {
337 let path = path.as_ref();
338 let content = fs::read_to_string(path).map_err(|e| {
339 CoreError::ConfigError(ErrorContext::new(format!(
340 "Failed to read config file {path}: {e}",
341 path = path.display()
342 )))
343 })?;
344
345 if let Ok(metadata) = fs::metadata(path) {
347 if let Ok(modified) = metadata.modified() {
348 if let Ok(mut watchers) = self.file_watchers.write() {
349 watchers.insert(path.to_string_lossy().to_string(), modified);
350 }
351 }
352 }
353
354 match path.extension().and_then(|ext| ext.to_str()) {
356 Some("json") => self.parse_json_config(&content),
357 Some("yaml" | "yml") => self.parse_yaml_config(&content),
358 Some("toml") => self.parse_toml_config(&content),
359 _ => Err(CoreError::ConfigError(ErrorContext::new(format!(
360 "Unsupported config file format: {path}",
361 path = path.display()
362 )))),
363 }
364 }
365
366 fn parse_json_config(&self, content: &str) -> CoreResult<()> {
368 Err(CoreError::ConfigError(ErrorContext::new(
370 "JSON parsing not implemented in this example",
371 )))
372 }
373
374 fn parse_yaml_config(&self, content: &str) -> CoreResult<()> {
376 Err(CoreError::ConfigError(ErrorContext::new(
378 "YAML parsing not implemented in this example",
379 )))
380 }
381
382 fn parse_toml_config(&self, content: &str) -> CoreResult<()> {
384 Err(CoreError::ConfigError(ErrorContext::new(
386 "TOML parsing not implemented in this example",
387 )))
388 }
389
390 pub fn set<S: Into<String>>(&self, key: S, value: S) -> CoreResult<()> {
392 let key = key.into();
393 let value = value.into();
394
395 if let Some(entry) = self.get_entry(&key)? {
397 if let Some(validator_name) = &entry.validator {
398 if let Some(validator) = self.validators.get(validator_name) {
399 let validation_result = validator.validate(&value);
400 if !validation_result.is_valid {
401 return Err(CoreError::ConfigError(ErrorContext::new(format!(
402 "Validation failed for {key}: {errors}",
403 errors = validation_result.errors.join(", ")
404 ))));
405 }
406 }
407 }
408 }
409
410 let mut entries = self.entries.write().map_err(|_| {
411 CoreError::ConfigError(ErrorContext::new("Failed to acquire config lock"))
412 })?;
413
414 let config_value = ConfigValue {
415 value,
416 source: ConfigSource::Override,
417 timestamp: SystemTime::now(),
418 is_sensitive: self.is_sensitive_key(&key),
419 description: None,
420 };
421
422 if let Some(entry) = entries.get_mut(&key) {
423 entry.value = config_value;
424 } else {
425 let entry = ConfigEntry {
426 key: key.clone(),
427 value: config_value,
428 validator: None,
429 hot_reloadable: true,
430 defaultvalue: None,
431 env_var: None,
432 };
433 entries.insert(key, entry);
434 }
435
436 Ok(())
437 }
438
439 pub fn get(&self, key: &str) -> CoreResult<Option<String>> {
441 let entries = self.entries.read().map_err(|_| {
442 CoreError::ConfigError(ErrorContext::new("Failed to acquire config lock"))
443 })?;
444
445 Ok(entries.get(key).map(|entry| entry.value.value.clone()))
446 }
447
448 pub fn get_entry(&self, key: &str) -> CoreResult<Option<ConfigEntry>> {
450 let entries = self.entries.read().map_err(|_| {
451 CoreError::ConfigError(ErrorContext::new("Failed to acquire config lock"))
452 })?;
453
454 Ok(entries.get(key).cloned())
455 }
456
457 pub fn get_typed<T>(&self, key: &str) -> CoreResult<Option<T>>
459 where
460 T: std::str::FromStr,
461 T::Err: fmt::Display,
462 {
463 if let Some(value_str) = self.get(key)? {
464 match value_str.parse::<T>() {
465 Ok(value) => Ok(Some(value)),
466 Err(e) => Err(CoreError::ConfigError(ErrorContext::new(format!(
467 "Failed to parse config value '{value_str}' for key '{key}': {e}"
468 )))),
469 }
470 } else {
471 Ok(None)
472 }
473 }
474
475 pub fn get_or_default<T>(&self, key: &str, default: T) -> CoreResult<T>
477 where
478 T: std::str::FromStr + Clone,
479 T::Err: fmt::Display,
480 {
481 match self.get_typed::<T>(key)? {
482 Some(value) => Ok(value),
483 None => Ok(default),
484 }
485 }
486
487 pub fn register(
489 &mut self,
490 key: String,
491 defaultvalue: Option<String>,
492 validator: Option<String>,
493 reloadable: bool,
494 description: Option<String>,
495 ) -> CoreResult<()> {
496 let mut entries = self.entries.write().map_err(|_| {
497 CoreError::ConfigError(ErrorContext::new("Failed to acquire config lock"))
498 })?;
499
500 if entries.contains_key(&key) {
502 return Ok(()); }
504
505 let value = ConfigValue {
506 value: defaultvalue.clone().unwrap_or_default(),
507 source: ConfigSource::Default,
508 timestamp: SystemTime::now(),
509 is_sensitive: self.is_sensitive_key(&key),
510 description: description.clone(),
511 };
512
513 let entry = ConfigEntry {
514 key: key.clone(),
515 value,
516 validator,
517 hot_reloadable: reloadable,
518 defaultvalue,
519 env_var: Some(format!("SCIRS_{}", key.to_uppercase())),
520 };
521
522 entries.insert(key, entry);
523 Ok(())
524 }
525
526 fn is_sensitive_key(&self, key: &str) -> bool {
528 let sensitive_patterns = ["password", "secret", "key", "token", "credential", "auth"];
529 let key_lower = key.to_lowercase();
530 sensitive_patterns
531 .iter()
532 .any(|pattern| key_lower.contains(pattern))
533 }
534
535 pub const fn environment(&self) -> &Environment {
537 &self.environment
538 }
539
540 pub fn set_feature_flag(
542 &mut self,
543 name: String,
544 enabled: bool,
545 rollout_percentage: f64,
546 ) -> CoreResult<()> {
547 let mut flags = self.feature_flags.write().map_err(|_| {
548 CoreError::ConfigError(ErrorContext::new("Failed to acquire feature flags lock"))
549 })?;
550
551 let flag = FeatureFlag {
552 name: name.clone(),
553 enabled,
554 rollout_percentage: rollout_percentage.clamp(0.0, 100.0),
555 environments: vec![self.environment.clone()],
556 user_groups: Vec::new(),
557 description: None,
558 last_modified: SystemTime::now(),
559 };
560
561 flags.insert(name, flag);
562 Ok(())
563 }
564
565 pub fn is_feature_enabled(&self, name: &str) -> bool {
567 if let Ok(flags) = self.feature_flags.read() {
568 if let Some(flag) = flags.get(name) {
569 return flag.enabled
570 && flag.environments.contains(&self.environment)
571 && self.check_rollout_percentage(flag.rollout_percentage);
572 }
573 }
574 false
575 }
576
577 fn check_rollout_percentage(&self, percentage: f64) -> bool {
579 use std::collections::hash_map::DefaultHasher;
582 use std::hash::{Hash, Hasher};
583
584 let mut hasher = DefaultHasher::new();
585 SystemTime::now().hash(&mut hasher);
586 let hash = hasher.finish();
587
588 (hash % 100) < percentage as u64
589 }
590
591 pub fn validate_all(&self) -> CoreResult<HashMap<String, ValidationResult>> {
593 let entries = self.entries.read().map_err(|_| {
594 CoreError::ConfigError(ErrorContext::new("Failed to acquire config lock"))
595 })?;
596
597 let mut results = HashMap::new();
598
599 for (key, entry) in entries.iter() {
600 if let Some(validator_name) = &entry.validator {
601 if let Some(validator) = self.validators.get(validator_name) {
602 let result = validator.validate(&entry.value.value);
603 results.insert(key.clone(), result);
604 }
605 }
606 }
607
608 Ok(results)
609 }
610
611 pub fn get_summary(&self) -> CoreResult<ConfigSummary> {
613 let entries = self.entries.read().map_err(|_| {
614 CoreError::ConfigError(ErrorContext::new("Failed to acquire config lock"))
615 })?;
616
617 let flags = self.feature_flags.read().map_err(|_| {
618 CoreError::ConfigError(ErrorContext::new("Failed to acquire feature flags lock"))
619 })?;
620
621 let total_configs = entries.len();
622 let env_configs = entries
623 .values()
624 .filter(|e| e.value.source == ConfigSource::Environment)
625 .count();
626 let file_configs = entries
627 .values()
628 .filter(|e| matches!(e.value.source, ConfigSource::File(_)))
629 .count();
630 let override_configs = entries
631 .values()
632 .filter(|e| e.value.source == ConfigSource::Override)
633 .count();
634 let sensitive_configs = entries.values().filter(|e| e.value.is_sensitive).count();
635
636 let total_flags = flags.len();
637 let enabled_flags = flags.values().filter(|f| f.enabled).count();
638
639 Ok(ConfigSummary {
640 environment: self.environment.clone(),
641 total_configs,
642 env_configs,
643 file_configs,
644 override_configs,
645 sensitive_configs,
646 total_flags,
647 enabled_flags,
648 hot_reload_enabled: self.hot_reload_enabled,
649 })
650 }
651
652 pub fn reload(&self) -> CoreResult<Vec<String>> {
654 if !self.hot_reload_enabled {
655 return Err(CoreError::ConfigError(ErrorContext::new(
656 "Hot reload is disabled",
657 )));
658 }
659
660 let mut reloaded_files = Vec::new();
661
662 if let Ok(watchers) = self.file_watchers.read() {
663 for (file_path, last_modified) in watchers.iter() {
664 if let Ok(metadata) = fs::metadata(file_path) {
665 if let Ok(current_modified) = metadata.modified() {
666 if current_modified > *last_modified {
667 if let Err(e) = self.load_from_file(file_path) {
669 eprintln!("Failed to reload config file {file_path}: {e}");
670 } else {
671 reloaded_files.push(file_path.clone());
672 }
673 }
674 }
675 }
676 }
677 }
678
679 Ok(reloaded_files)
680 }
681}
682
683impl Default for ProductionConfig {
684 fn default() -> Self {
685 Self::new()
686 }
687}
688
689#[derive(Debug, Clone)]
691pub struct ConfigSummary {
692 pub environment: Environment,
693 pub total_configs: usize,
694 pub env_configs: usize,
695 pub file_configs: usize,
696 pub override_configs: usize,
697 pub sensitive_configs: usize,
698 pub total_flags: usize,
699 pub enabled_flags: usize,
700 pub hot_reload_enabled: bool,
701}
702
703impl fmt::Display for ConfigSummary {
704 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
705 writeln!(f, "Configuration Summary:")?;
706 writeln!(f, " Environment: {}", self.environment)?;
707 writeln!(f, " Total configurations: {}", self.total_configs)?;
708 writeln!(f, " From environment: {}", self.env_configs)?;
709 writeln!(f, " From files: {}", self.file_configs)?;
710 writeln!(f, " Runtime overrides: {}", self.override_configs)?;
711 writeln!(f, " Sensitive configs: {}", self.sensitive_configs)?;
712 writeln!(f, " Feature flags:")?;
713 writeln!(f, " Total: {}", self.total_flags)?;
714 writeln!(f, " Enabled: {}", self.enabled_flags)?;
715 writeln!(
716 f,
717 " Hot reload: {}",
718 if self.hot_reload_enabled {
719 "enabled"
720 } else {
721 "disabled"
722 }
723 )?;
724 Ok(())
725 }
726}
727
728static GLOBAL_CONFIG: std::sync::LazyLock<ProductionConfig> = std::sync::LazyLock::new(|| {
730 let mut config = ProductionConfig::new();
731
732 if let Err(e) = config.load_from_env() {
734 eprintln!("Warning: Failed to load configuration from environment: {e}");
735 }
736
737 let _ = config.register(
739 "log_level".to_string(),
740 Some("info".to_string()),
741 None,
742 true,
743 Some("Logging level (trace, debug, info, warn, error)".to_string()),
744 );
745
746 let _ = config.register(
747 "max_memory_mb".to_string(),
748 Some("1024".to_string()),
749 Some("positive_int".to_string()),
750 true,
751 Some("Maximum memory usage in megabytes".to_string()),
752 );
753
754 let _ = config.register(
755 "worker_threads".to_string(),
756 Some("4".to_string()),
757 Some("positive_int".to_string()),
758 false,
759 Some("Number of worker threads".to_string()),
760 );
761
762 config
763});
764
765#[allow(dead_code)]
767pub fn global_config() -> &'static ProductionConfig {
768 &GLOBAL_CONFIG
769}
770
771#[macro_export]
774macro_rules! config_get {
775 ($key:expr) => {
776 $crate::config::production::global_config().get($key)
777 };
778 ($key:expr, $type:ty) => {
779 $crate::config::production::global_config().get_typed::<$type>($key)
780 };
781 ($key:expr, $type:ty, $default:expr) => {
782 $crate::config::production::global_config().get_or_default::<$type>($key, $default)
783 };
784}
785
786#[macro_export]
788macro_rules! config_set {
789 ($key:expr, $value:expr) => {
790 $crate::config::production::global_config().set($key, $value)
791 };
792}
793
794#[macro_export]
796macro_rules! feature_enabled {
797 ($feature:expr) => {
798 $crate::config::production::global_config().is_feature_enabled($feature)
799 };
800}
801
802#[cfg(test)]
803mod tests {
804 use super::*;
805
806 #[test]
807 fn test_environment_parsing() {
808 assert_eq!(
809 Environment::from_str("development"),
810 Ok(Environment::Development)
811 );
812 assert_eq!(Environment::from_str("dev"), Ok(Environment::Development));
813 assert_eq!(
814 Environment::from_str("production"),
815 Ok(Environment::Production)
816 );
817 assert_eq!(
818 Environment::from_str("custom"),
819 Ok(Environment::Custom("custom".to_string()))
820 );
821 }
822
823 #[test]
824 fn test_validators() {
825 let validator = PositiveIntValidator;
826
827 let result = validator.validate("42");
828 assert!(result.is_valid);
829
830 let result = validator.validate("-5");
831 assert!(!result.is_valid);
832
833 let result = validator.validate("not_a_number");
834 assert!(!result.is_valid);
835 }
836
837 #[test]
838 fn test_config_operations() {
839 let config = ProductionConfig::new();
840
841 config
843 .set("test_key", "test_value")
844 .expect("Operation failed");
845 assert_eq!(
846 config.get("test_key").expect("Operation failed"),
847 Some("test_value".to_string())
848 );
849
850 config.set("test_number", "42").expect("Operation failed");
852 assert_eq!(
853 config
854 .get_typed::<i32>("test_number")
855 .expect("Operation failed"),
856 Some(42)
857 );
858
859 assert_eq!(
861 config
862 .get_or_default("missing_key", 100i32)
863 .expect("Operation failed"),
864 100
865 );
866 }
867
868 #[test]
869 fn test_feature_flags() {
870 let mut config = ProductionConfig::new();
871
872 config
873 .set_feature_flag("test_feature".to_string(), true, 100.0)
874 .expect("Operation failed");
875 assert!(config.is_feature_enabled("test_feature"));
876
877 config
878 .set_feature_flag("disabled_feature".to_string(), false, 100.0)
879 .expect("Operation failed");
880 assert!(!config.is_feature_enabled("disabled_feature"));
881 }
882
883 #[test]
884 fn test_sensitive_key_detection() {
885 let config = ProductionConfig::new();
886
887 assert!(config.is_sensitive_key("api_password"));
888 assert!(config.is_sensitive_key("SECRET_KEY"));
889 assert!(config.is_sensitive_key("auth_token"));
890 assert!(!config.is_sensitive_key("log_level"));
891 assert!(!config.is_sensitive_key("max_connections"));
892 }
893
894 #[test]
895 fn test_config_registration() {
896 let mut config = ProductionConfig::new();
897
898 config
899 .register(
900 "test_config".to_string(),
901 Some("defaultvalue".to_string()),
902 Some("positive_int".to_string()),
903 true,
904 Some("Test configuration".to_string()),
905 )
906 .expect("Operation failed");
907
908 let entry = config
909 .get_entry("test_config")
910 .expect("Operation failed")
911 .expect("Operation failed");
912 assert_eq!(entry.defaultvalue, Some("defaultvalue".to_string()));
913 assert_eq!(entry.validator, Some("positive_int".to_string()));
914 assert!(entry.hot_reloadable);
915 }
916}