1use std::any::TypeId;
8use crate::error::{ContextError, ContextResult};
9use dashmap::{DashMap, DashSet};
10use parking_lot::RwLock;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::path::Path;
14use std::sync::Arc;
15use verdure_ioc::ComponentInstance;
16
17pub trait ConfigInitializer {
18 fn from_config_manager(config_manager: Arc<ConfigManager>) -> ContextResult<Self>
19 where
20 Self: Sized;
21
22 fn config_module_key() -> &'static str;
23}
24
25pub struct ConfigFactory {
26 pub type_id: fn() -> TypeId,
27 pub create_fn: fn(Arc<ConfigManager>) -> ContextResult<ComponentInstance>,
28}
29
30inventory::collect!(ConfigFactory);
31
32#[derive(Debug, Clone, Copy)]
34enum ConfigFileFormat {
35 Toml,
36 Yaml,
37 Properties,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
45pub enum ConfigSource {
46 TomlFile(String),
48 YamlFile(String),
50 PropertiesFile(String),
52 ConfigFile(String),
54 Environment,
56 CommandLine,
58 Properties(HashMap<String, String>),
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
67pub enum ConfigValue {
68 String(String),
70 Integer(i64),
72 Float(f64),
74 Boolean(bool),
76 Array(Vec<ConfigValue>),
78 Object(HashMap<String, ConfigValue>),
80}
81
82impl ConfigValue {
83 pub fn as_string(&self) -> Option<String> {
97 match self {
98 ConfigValue::String(s) => Some(s.clone()),
99 ConfigValue::Integer(i) => Some(i.to_string()),
100 ConfigValue::Float(f) => Some(f.to_string()),
101 ConfigValue::Boolean(b) => Some(b.to_string()),
102 _ => None,
103 }
104 }
105
106 pub fn as_integer(&self) -> Option<i64> {
120 match self {
121 ConfigValue::Integer(i) => Some(*i),
122 ConfigValue::String(s) => s.parse().ok(),
123 _ => None,
124 }
125 }
126
127 pub fn as_float(&self) -> Option<f64> {
141 match self {
142 ConfigValue::Float(f) => Some(*f),
143 ConfigValue::Integer(i) => Some(*i as f64),
144 ConfigValue::String(s) => s.parse().ok(),
145 _ => None,
146 }
147 }
148
149 pub fn as_boolean(&self) -> Option<bool> {
163 match self {
164 ConfigValue::Boolean(b) => Some(*b),
165 ConfigValue::String(s) => match s.to_lowercase().as_str() {
166 "true" | "yes" | "on" | "1" => Some(true),
167 "false" | "no" | "off" | "0" => Some(false),
168 _ => None,
169 },
170 ConfigValue::Integer(i) => Some(*i != 0),
171 _ => None,
172 }
173 }
174
175 pub fn as_array(&self) -> Option<&Vec<ConfigValue>> {
189 match self {
190 ConfigValue::Array(arr) => Some(arr),
191 _ => None,
192 }
193 }
194
195 pub fn as_object(&self) -> Option<&HashMap<String, ConfigValue>> {
210 match self {
211 ConfigValue::Object(obj) => Some(obj),
212 _ => None,
213 }
214 }
215}
216
217#[derive(Clone)]
242pub struct ConfigManager {
243 sources: Arc<RwLock<Vec<ConfigSource>>>,
245
246 cache: Arc<DashMap<String, ConfigValue>>,
248
249 file_cache: Arc<DashMap<String, HashMap<String, String>>>,
251
252 dirty_keys: Arc<DashSet<String>>,
254}
255
256impl ConfigManager {
257 pub fn new() -> Self {
259 Self {
260 sources: Arc::new(RwLock::new(Vec::new())),
261 cache: Arc::new(DashMap::new()),
262 file_cache: Arc::new(DashMap::new()),
263 dirty_keys: Arc::new(DashSet::new()),
264 }
265 }
266
267 pub fn add_source(&self, source: ConfigSource) -> ContextResult<()> {
269 {
270 let mut sources = self.sources.write();
271 sources.push(source);
272 }
273
274 self.invalidate_cache();
275 Ok(())
276 }
277
278 pub fn load_from_toml_file<P: AsRef<Path>>(&mut self, path: P) -> ContextResult<()> {
293 let path_str = path.as_ref().to_string_lossy().to_string();
294 self.add_source(ConfigSource::TomlFile(path_str))
295 }
296
297 pub fn load_from_yaml_file<P: AsRef<Path>>(&mut self, path: P) -> ContextResult<()> {
312 let path_str = path.as_ref().to_string_lossy().to_string();
313 self.add_source(ConfigSource::YamlFile(path_str))
314 }
315
316 pub fn load_from_properties_file<P: AsRef<Path>>(&mut self, path: P) -> ContextResult<()> {
331 let path_str = path.as_ref().to_string_lossy().to_string();
332 self.add_source(ConfigSource::PropertiesFile(path_str))
333 }
334
335 pub fn load_from_config_file<P: AsRef<Path>>(&mut self, path: P) -> ContextResult<()> {
358 let path_str = path.as_ref().to_string_lossy().to_string();
359 self.add_source(ConfigSource::ConfigFile(path_str))
360 }
361
362 pub fn get(&self, key: &str) -> Option<ConfigValue> {
389 if let Some(cached) = self.cache.get(key) {
390 return Some(cached.clone());
391 }
392
393 self.get_and_cache(key)
394 }
395
396 fn get_and_cache(&self, key: &str) -> Option<ConfigValue> {
398 let sources = self.sources.read();
399 for source in sources.iter().rev() {
400 if let Some(value) = self.get_from_source(source, key) {
401 self.cache.insert(key.to_string(), value.clone());
402 return Some(value);
403 }
404 }
405
406 None
407 }
408
409 pub fn get_string(&self, key: &str) -> ContextResult<String> {
438 self.get(key)
439 .and_then(|v| v.as_string())
440 .ok_or_else(|| ContextError::configuration_not_found(key))
441 }
442
443 pub fn get_integer(&self, key: &str) -> ContextResult<i64> {
472 self.get(key)
473 .and_then(|v| v.as_integer())
474 .ok_or_else(|| ContextError::configuration_not_found(key))
475 }
476
477 pub fn get_float(&self, key: &str) -> ContextResult<f64> {
491 self.get(key)
492 .and_then(|v| v.as_float())
493 .ok_or_else(|| ContextError::configuration_not_found(key))
494 }
495
496 pub fn get_boolean(&self, key: &str) -> ContextResult<bool> {
525 self.get(key)
526 .and_then(|v| v.as_boolean())
527 .ok_or_else(|| ContextError::configuration_not_found(key))
528 }
529
530 pub fn get_string_or_default(&self, key: &str, default: &str) -> String {
547 self.get_string(key).unwrap_or_else(|_| default.to_string())
548 }
549
550 pub fn get_integer_or_default(&self, key: &str, default: i64) -> i64 {
557 self.get_integer(key).unwrap_or(default)
558 }
559
560 pub fn get_boolean_or_default(&self, key: &str, default: bool) -> bool {
567 self.get_boolean(key).unwrap_or(default)
568 }
569
570 pub fn set(&self, key: &str, value: ConfigValue) {
572 self.cache.insert(key.to_string(), value);
573 }
574
575 pub fn sources_count(&self) -> usize {
581 self.sources.read().len()
582 }
583
584 pub fn invalidate_cache(&self) {
586 self.cache.clear();
587 self.dirty_keys.clear();
588 }
589
590 pub fn invalidate_keys(&self, keys: &[String]) {
592 for key in keys {
593 self.cache.remove(key);
594 self.dirty_keys.insert(key.clone());
595 }
596 }
597
598 fn get_from_source(&self, source: &ConfigSource, key: &str) -> Option<ConfigValue> {
600 match source {
601 ConfigSource::Properties(props) => {
602 props.get(key).map(|v| ConfigValue::String(v.clone()))
603 }
604 ConfigSource::Environment => {
605 let env_key = key.to_uppercase().replace('.', "_");
607 std::env::var(&env_key).ok().map(|v| ConfigValue::String(v))
608 }
609 ConfigSource::TomlFile(path) => self
610 .load_file_config(path, ConfigFileFormat::Toml)
611 .and_then(|props| props.get(key).map(|v| ConfigValue::String(v.clone()))),
612 ConfigSource::YamlFile(path) => self
613 .load_file_config(path, ConfigFileFormat::Yaml)
614 .and_then(|props| props.get(key).map(|v| ConfigValue::String(v.clone()))),
615 ConfigSource::PropertiesFile(path) => self
616 .load_file_config(path, ConfigFileFormat::Properties)
617 .and_then(|props| props.get(key).map(|v| ConfigValue::String(v.clone()))),
618 ConfigSource::ConfigFile(path) => self
619 .load_file_config_auto_detect(path)
620 .and_then(|props| props.get(key).map(|v| ConfigValue::String(v.clone()))),
621 _ => None, }
623 }
624
625 fn load_file_config(
627 &self,
628 path: &str,
629 format: ConfigFileFormat,
630 ) -> Option<HashMap<String, String>> {
631 let content = std::fs::read_to_string(path).ok()?;
632
633 match format {
634 ConfigFileFormat::Toml => {
635 let toml_value: toml::Value = toml::from_str(&content).ok()?;
636 self.toml_value_to_config_map(&toml_value, "").ok()
637 }
638 ConfigFileFormat::Yaml => {
639 let yaml_value: serde_yaml::Value = serde_yaml::from_str(&content).ok()?;
640 self.yaml_value_to_config_map(&yaml_value, "").ok()
641 }
642 ConfigFileFormat::Properties => self.parse_properties(&content).ok(),
643 }
644 }
645
646 fn load_file_config_auto_detect(&self, path: &str) -> Option<HashMap<String, String>> {
648 let path_lower = path.to_lowercase();
649
650 if path_lower.ends_with(".toml") {
652 return self.load_file_config(path, ConfigFileFormat::Toml);
653 } else if path_lower.ends_with(".yaml") || path_lower.ends_with(".yml") {
654 return self.load_file_config(path, ConfigFileFormat::Yaml);
655 } else if path_lower.ends_with(".properties") {
656 return self.load_file_config(path, ConfigFileFormat::Properties);
657 }
658
659 if let Some(config) = self.load_file_config(path, ConfigFileFormat::Toml) {
661 return Some(config);
662 }
663
664 if let Some(config) = self.load_file_config(path, ConfigFileFormat::Yaml) {
665 return Some(config);
666 }
667
668 self.load_file_config(path, ConfigFileFormat::Properties)
669 }
670
671 fn yaml_value_to_config_map(
673 &self,
674 value: &serde_yaml::Value,
675 prefix: &str,
676 ) -> ContextResult<HashMap<String, String>> {
677 let mut map = HashMap::new();
678
679 match value {
680 serde_yaml::Value::Mapping(mapping) => {
681 for (key, val) in mapping {
682 if let Some(key_str) = key.as_str() {
683 let full_key = if prefix.is_empty() {
684 key_str.to_string()
685 } else {
686 format!("{}.{}", prefix, key_str)
687 };
688
689 match val {
690 serde_yaml::Value::Mapping(_) => {
691 let nested_map = self.yaml_value_to_config_map(val, &full_key)?;
693 map.extend(nested_map);
694 }
695 _ => {
696 map.insert(full_key, self.yaml_value_to_string(val));
698 }
699 }
700 }
701 }
702 }
703 _ => {
704 if !prefix.is_empty() {
706 map.insert(prefix.to_string(), self.yaml_value_to_string(value));
707 }
708 }
709 }
710
711 Ok(map)
712 }
713
714 fn yaml_value_to_string(&self, value: &serde_yaml::Value) -> String {
716 match value {
717 serde_yaml::Value::String(s) => s.clone(),
718 serde_yaml::Value::Number(n) => n.to_string(),
719 serde_yaml::Value::Bool(b) => b.to_string(),
720 serde_yaml::Value::Sequence(arr) => {
721 arr.iter()
723 .map(|v| self.yaml_value_to_string(v))
724 .collect::<Vec<_>>()
725 .join(",")
726 }
727 serde_yaml::Value::Null => "".to_string(),
728 _ => format!("{:?}", value),
729 }
730 }
731
732 fn parse_properties(&self, content: &str) -> ContextResult<HashMap<String, String>> {
734 let mut map = HashMap::new();
735
736 for line in content.lines() {
737 let line = line.trim();
738
739 if line.is_empty() || line.starts_with('#') || line.starts_with('!') {
741 continue;
742 }
743
744 if let Some(separator_pos) = line.find('=').or_else(|| line.find(':')) {
746 let key = line[..separator_pos].trim().to_string();
747 let value = line[separator_pos + 1..].trim().to_string();
748
749 let processed_value = self.process_properties_value(&value);
751
752 map.insert(key, processed_value);
753 }
754 }
755
756 Ok(map)
757 }
758
759 fn process_properties_value(&self, value: &str) -> String {
761 value
763 .replace("\\n", "\n")
764 .replace("\\t", "\t")
765 .replace("\\r", "\r")
766 .replace("\\\\", "\\")
767 }
768 fn toml_value_to_config_map(
769 &self,
770 value: &toml::Value,
771 prefix: &str,
772 ) -> ContextResult<HashMap<String, String>> {
773 let mut map = HashMap::new();
774
775 match value {
776 toml::Value::Table(table) => {
777 for (key, val) in table {
778 let full_key = if prefix.is_empty() {
779 key.clone()
780 } else {
781 format!("{}.{}", prefix, key)
782 };
783
784 match val {
785 toml::Value::Table(_) => {
786 let nested_map = self.toml_value_to_config_map(val, &full_key)?;
788 map.extend(nested_map);
789 }
790 _ => {
791 map.insert(full_key, self.toml_value_to_string(val));
793 }
794 }
795 }
796 }
797 _ => {
798 if !prefix.is_empty() {
800 map.insert(prefix.to_string(), self.toml_value_to_string(value));
801 }
802 }
803 }
804
805 Ok(map)
806 }
807
808 fn toml_value_to_string(&self, value: &toml::Value) -> String {
810 match value {
811 toml::Value::String(s) => s.clone(),
812 toml::Value::Integer(i) => i.to_string(),
813 toml::Value::Float(f) => f.to_string(),
814 toml::Value::Boolean(b) => b.to_string(),
815 toml::Value::Array(arr) => {
816 arr.iter()
818 .map(|v| self.toml_value_to_string(v))
819 .collect::<Vec<_>>()
820 .join(",")
821 }
822 _ => value.to_string(),
823 }
824 }
825}
826
827impl Default for ConfigManager {
828 fn default() -> Self {
829 Self::new()
830 }
831}
832
833#[cfg(test)]
834mod tests {
835 use super::*;
836
837 #[test]
838 fn test_config_value_conversions() {
839 let value = ConfigValue::String("hello".to_string());
841 assert_eq!(value.as_string(), Some("hello".to_string()));
842
843 let value = ConfigValue::Integer(42);
845 assert_eq!(value.as_integer(), Some(42));
846 assert_eq!(value.as_string(), Some("42".to_string()));
847 assert_eq!(value.as_float(), Some(42.0));
848
849 let value = ConfigValue::Boolean(true);
851 assert_eq!(value.as_boolean(), Some(true));
852 assert_eq!(value.as_string(), Some("true".to_string()));
853
854 let value = ConfigValue::String("yes".to_string());
856 assert_eq!(value.as_boolean(), Some(true));
857
858 let value = ConfigValue::String("false".to_string());
859 assert_eq!(value.as_boolean(), Some(false));
860
861 let value = ConfigValue::String("123".to_string());
863 assert_eq!(value.as_integer(), Some(123));
864
865 let value = ConfigValue::String("not_a_number".to_string());
867 assert_eq!(value.as_integer(), None);
868
869 let value = ConfigValue::Array(vec![]);
870 assert_eq!(value.as_string(), None);
871 }
872
873 #[test]
874 fn test_config_manager_creation() {
875 let manager = ConfigManager::new();
876 assert_eq!(manager.sources_count(), 0);
877 }
878
879 #[test]
880 fn test_config_manager_properties_source() {
881 let manager = ConfigManager::new();
882 let mut props = HashMap::new();
883 props.insert("app.name".to_string(), "TestApp".to_string());
884 props.insert("app.port".to_string(), "8080".to_string());
885 props.insert("app.debug".to_string(), "true".to_string());
886
887 manager.add_source(ConfigSource::Properties(props)).unwrap();
888
889 assert_eq!(manager.get_string("app.name").unwrap(), "TestApp");
890 assert_eq!(manager.get_integer("app.port").unwrap(), 8080);
891 assert_eq!(manager.get_boolean("app.debug").unwrap(), true);
892
893 assert!(manager.get_string("missing.key").is_err());
895 }
896
897 #[test]
898 fn test_config_manager_defaults() {
899 let manager = ConfigManager::new();
900
901 assert_eq!(
902 manager.get_string_or_default("missing.key", "default"),
903 "default"
904 );
905 assert_eq!(manager.get_integer_or_default("missing.key", 42), 42);
906 assert_eq!(manager.get_boolean_or_default("missing.key", true), true);
907 }
908
909 #[test]
910 fn test_config_manager_source_precedence() {
911 let manager = ConfigManager::new();
912
913 let mut props1 = HashMap::new();
915 props1.insert("app.name".to_string(), "App1".to_string());
916 props1.insert("app.version".to_string(), "1.0".to_string());
917 manager
918 .add_source(ConfigSource::Properties(props1))
919 .unwrap();
920
921 let mut props2 = HashMap::new();
923 props2.insert("app.name".to_string(), "App2".to_string());
924 props2.insert("app.port".to_string(), "8080".to_string());
925 manager
926 .add_source(ConfigSource::Properties(props2))
927 .unwrap();
928
929 assert_eq!(manager.get_string("app.name").unwrap(), "App2");
931 assert_eq!(manager.get_string("app.version").unwrap(), "1.0"); assert_eq!(manager.get_string("app.port").unwrap(), "8080"); }
934
935 #[test]
936 fn test_config_manager_cache() {
937 let manager = ConfigManager::new();
938 let mut props = HashMap::new();
939 props.insert("test.key".to_string(), "test.value".to_string());
940
941 manager.add_source(ConfigSource::Properties(props)).unwrap();
942
943 assert_eq!(manager.get_string("test.key").unwrap(), "test.value");
945
946 assert_eq!(manager.get_string("test.key").unwrap(), "test.value");
948
949 manager.set(
951 "runtime.key",
952 ConfigValue::String("runtime.value".to_string()),
953 );
954 assert_eq!(manager.get_string("runtime.key").unwrap(), "runtime.value");
955 }
956
957 #[test]
958 fn test_config_manager_cache_invalidation() {
959 let manager = ConfigManager::new();
960 let mut props = HashMap::new();
961 props.insert("test.key".to_string(), "test.value".to_string());
962
963 manager.add_source(ConfigSource::Properties(props)).unwrap();
964
965 assert_eq!(manager.get_string("test.key").unwrap(), "test.value");
967
968 manager.invalidate_cache();
970
971 assert_eq!(manager.get_string("test.key").unwrap(), "test.value");
973 }
974
975 #[test]
976 fn test_yaml_parsing() {
977 let yaml_content = r#"
978app:
979 name: "TestApp"
980 port: 8080
981 features:
982 - "auth"
983 - "logging"
984database:
985 host: "localhost"
986 port: 5432
987 ssl: true
988"#;
989
990 let manager = ConfigManager::new();
991 let yaml_value: serde_yaml::Value = serde_yaml::from_str(yaml_content).unwrap();
992 let config_map = manager.yaml_value_to_config_map(&yaml_value, "").unwrap();
993
994 assert_eq!(config_map.get("app.name"), Some(&"TestApp".to_string()));
995 assert_eq!(config_map.get("app.port"), Some(&"8080".to_string()));
996 assert_eq!(
997 config_map.get("database.host"),
998 Some(&"localhost".to_string())
999 );
1000 assert_eq!(config_map.get("database.ssl"), Some(&"true".to_string()));
1001 assert_eq!(
1002 config_map.get("app.features"),
1003 Some(&"auth,logging".to_string())
1004 );
1005 }
1006
1007 #[test]
1008 fn test_properties_parsing() {
1009 let properties_content = r#"
1010# Application configuration
1011app.name=TestApp
1012app.port=8080
1013app.debug=true
1014
1015# Database configuration
1016database.host=localhost
1017database.port=5432
1018database.ssl=true
1019
1020# Comments and empty lines should be ignored
1021! This is another comment style
1022"#;
1023
1024 let manager = ConfigManager::new();
1025 let config_map = manager.parse_properties(properties_content).unwrap();
1026
1027 assert_eq!(config_map.get("app.name"), Some(&"TestApp".to_string()));
1028 assert_eq!(config_map.get("app.port"), Some(&"8080".to_string()));
1029 assert_eq!(config_map.get("app.debug"), Some(&"true".to_string()));
1030 assert_eq!(
1031 config_map.get("database.host"),
1032 Some(&"localhost".to_string())
1033 );
1034 assert_eq!(config_map.get("database.port"), Some(&"5432".to_string()));
1035 assert_eq!(config_map.get("database.ssl"), Some(&"true".to_string()));
1036 }
1037
1038 #[test]
1039 fn test_properties_escape_sequences() {
1040 let properties_content = r#"
1041message.welcome=Hello\nWorld\tTest
1042file.path=C:\\Users\\Test
1043"#;
1044
1045 let manager = ConfigManager::new();
1046 let config_map = manager.parse_properties(properties_content).unwrap();
1047
1048 assert_eq!(
1049 config_map.get("message.welcome"),
1050 Some(&"Hello\nWorld\tTest".to_string())
1051 );
1052 assert_eq!(
1053 config_map.get("file.path"),
1054 Some(&"C:\\Users\\Test".to_string())
1055 );
1056 }
1057
1058 #[test]
1059 fn test_config_source_types() {
1060 let manager = ConfigManager::new();
1061
1062 let mut props = HashMap::new();
1064 props.insert("source.type".to_string(), "properties".to_string());
1065
1066 manager.add_source(ConfigSource::Properties(props)).unwrap();
1067
1068 assert_eq!(manager.get_string("source.type").unwrap(), "properties");
1069 }
1070
1071 #[test]
1072 fn test_multiple_config_formats() {
1073 let manager = ConfigManager::new();
1074
1075 let mut props = HashMap::new();
1077 props.insert("app.name".to_string(), "PropsApp".to_string());
1078 props.insert("app.version".to_string(), "1.0".to_string());
1079 manager.add_source(ConfigSource::Properties(props)).unwrap();
1080
1081 let mut override_props = HashMap::new();
1083 override_props.insert("app.name".to_string(), "OverrideApp".to_string());
1084 override_props.insert("app.env".to_string(), "test".to_string());
1085 manager
1086 .add_source(ConfigSource::Properties(override_props))
1087 .unwrap();
1088
1089 assert_eq!(manager.get_string("app.name").unwrap(), "OverrideApp");
1091 assert_eq!(manager.get_string("app.version").unwrap(), "1.0"); assert_eq!(manager.get_string("app.env").unwrap(), "test"); }
1094}