Skip to main content

verdure_context/
config.rs

1//! Configuration management system
2//!
3//! This module provides configuration management functionality for the Verdure context system.
4//! It supports hierarchical configuration sources, property binding, type-safe configuration
5//! access, and integration with environment profiles.
6
7use 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/// Configuration file formats
33#[derive(Debug, Clone, Copy)]
34enum ConfigFileFormat {
35    Toml,
36    Yaml,
37    Properties,
38}
39
40/// Configuration source types
41///
42/// `ConfigSource` represents different sources from which configuration
43/// can be loaded, supporting various formats and locations.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub enum ConfigSource {
46    /// Configuration from a TOML file
47    TomlFile(String),
48    /// Configuration from a YAML file
49    YamlFile(String),
50    /// Configuration from a Properties file
51    PropertiesFile(String),
52    /// Configuration from any file (auto-detect format)
53    ConfigFile(String),
54    /// Configuration from environment variables
55    Environment,
56    /// Configuration from command line arguments
57    CommandLine,
58    /// In-memory configuration properties
59    Properties(HashMap<String, String>),
60}
61
62/// Configuration value types
63///
64/// `ConfigValue` represents different types of configuration values
65/// that can be stored and retrieved from the configuration system.
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub enum ConfigValue {
68    /// String value
69    String(String),
70    /// Integer value
71    Integer(i64),
72    /// Float value
73    Float(f64),
74    /// Boolean value
75    Boolean(bool),
76    /// Array of values
77    Array(Vec<ConfigValue>),
78    /// Nested object/map
79    Object(HashMap<String, ConfigValue>),
80}
81
82impl ConfigValue {
83    /// Converts the value to a string if possible
84    ///
85    /// # Examples
86    ///
87    /// ```rust
88    /// use verdure_context::ConfigValue;
89    ///
90    /// let value = ConfigValue::String("hello".to_string());
91    /// assert_eq!(value.as_string(), Some("hello".to_string()));
92    ///
93    /// let value = ConfigValue::Integer(42);
94    /// assert_eq!(value.as_string(), Some("42".to_string()));
95    /// ```
96    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    /// Converts the value to an integer if possible
107    ///
108    /// # Examples
109    ///
110    /// ```rust
111    /// use verdure_context::ConfigValue;
112    ///
113    /// let value = ConfigValue::Integer(42);
114    /// assert_eq!(value.as_integer(), Some(42));
115    ///
116    /// let value = ConfigValue::String("123".to_string());
117    /// assert_eq!(value.as_integer(), Some(123));
118    /// ```
119    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    /// Converts the value to a float if possible
128    ///
129    /// # Examples
130    ///
131    /// ```rust
132    /// use verdure_context::ConfigValue;
133    ///
134    /// let value = ConfigValue::Float(3.14);
135    /// assert_eq!(value.as_float(), Some(3.14));
136    ///
137    /// let value = ConfigValue::Integer(42);
138    /// assert_eq!(value.as_float(), Some(42.0));
139    /// ```
140    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    /// Converts the value to a boolean if possible
150    ///
151    /// # Examples
152    ///
153    /// ```rust
154    /// use verdure_context::ConfigValue;
155    ///
156    /// let value = ConfigValue::Boolean(true);
157    /// assert_eq!(value.as_boolean(), Some(true));
158    ///
159    /// let value = ConfigValue::String("true".to_string());
160    /// assert_eq!(value.as_boolean(), Some(true));
161    /// ```
162    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    /// Converts the value to an array if possible
176    ///
177    /// # Examples
178    ///
179    /// ```rust
180    /// use verdure_context::ConfigValue;
181    ///
182    /// let value = ConfigValue::Array(vec![
183    ///     ConfigValue::String("a".to_string()),
184    ///     ConfigValue::String("b".to_string()),
185    /// ]);
186    /// assert!(value.as_array().is_some());
187    /// ```
188    pub fn as_array(&self) -> Option<&Vec<ConfigValue>> {
189        match self {
190            ConfigValue::Array(arr) => Some(arr),
191            _ => None,
192        }
193    }
194
195    /// Converts the value to an object/map if possible
196    ///
197    /// # Examples
198    ///
199    /// ```rust
200    /// use verdure_context::ConfigValue;
201    /// use std::collections::HashMap;
202    ///
203    /// let mut obj = HashMap::new();
204    /// obj.insert("key".to_string(), ConfigValue::String("value".to_string()));
205    ///
206    /// let value = ConfigValue::Object(obj);
207    /// assert!(value.as_object().is_some());
208    /// ```
209    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/// Configuration manager
218///
219/// `ConfigManager` provides comprehensive configuration management functionality,
220/// including loading from multiple sources, hierarchical property resolution,
221/// type-safe access, and integration with environment profiles.
222///
223/// # Examples
224///
225/// ```rust
226/// use verdure_context::{ConfigManager, ConfigSource};
227/// use std::collections::HashMap;
228///
229/// let mut manager = ConfigManager::new();
230///
231/// // Add configuration from properties
232/// let mut props = HashMap::new();
233/// props.insert("app.name".to_string(), "MyApp".to_string());
234/// props.insert("app.port".to_string(), "8080".to_string());
235///
236/// manager.add_source(ConfigSource::Properties(props)).unwrap();
237///
238/// assert_eq!(manager.get_string("app.name").unwrap(), "MyApp");
239/// assert_eq!(manager.get_integer("app.port").unwrap(), 8080);
240/// ```
241#[derive(Clone)]
242pub struct ConfigManager {
243    /// Configuration sources in precedence order (last added = highest precedence)
244    sources: Arc<RwLock<Vec<ConfigSource>>>,
245    
246    /// Primary configuration cache
247    cache: Arc<DashMap<String, ConfigValue>>,
248    
249    /// File content cache to avoid repeated file I/O
250    file_cache: Arc<DashMap<String, HashMap<String, String>>>,
251    
252    /// Cache invalidation tracking
253    dirty_keys: Arc<DashSet<String>>,
254}
255
256impl ConfigManager {
257    /// Creates a new configuration manager
258    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    /// Adds a configuration source
268    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    /// Loads configuration from a TOML file
279    ///
280    /// # Arguments
281    ///
282    /// * `path` - Path to the TOML configuration file
283    ///
284    /// # Examples
285    ///
286    /// ```rust,no_run
287    /// use verdure_context::ConfigManager;
288    ///
289    /// let mut manager = ConfigManager::new();
290    /// manager.load_from_toml_file("config/app.toml").unwrap();
291    /// ```
292    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    /// Loads configuration from a YAML file
298    ///
299    /// # Arguments
300    ///
301    /// * `path` - Path to the YAML configuration file
302    ///
303    /// # Examples
304    ///
305    /// ```rust,no_run
306    /// use verdure_context::ConfigManager;
307    ///
308    /// let mut manager = ConfigManager::new();
309    /// manager.load_from_yaml_file("config/app.yaml").unwrap();
310    /// ```
311    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    /// Loads configuration from a Properties file
317    ///
318    /// # Arguments
319    ///
320    /// * `path` - Path to the Properties configuration file
321    ///
322    /// # Examples
323    ///
324    /// ```rust,no_run
325    /// use verdure_context::ConfigManager;
326    ///
327    /// let mut manager = ConfigManager::new();
328    /// manager.load_from_properties_file("config/app.properties").unwrap();
329    /// ```
330    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    /// Loads configuration from a file with automatic format detection
336    ///
337    /// The format is detected based on the file extension:
338    /// - `.toml` -> TOML format
339    /// - `.yaml` or `.yml` -> YAML format  
340    /// - `.properties` -> Properties format
341    /// - Others -> Attempts to parse as TOML first, then YAML, then Properties
342    ///
343    /// # Arguments
344    ///
345    /// * `path` - Path to the configuration file
346    ///
347    /// # Examples
348    ///
349    /// ```rust,no_run
350    /// use verdure_context::ConfigManager;
351    ///
352    /// let mut manager = ConfigManager::new();
353    /// manager.load_from_config_file("config/app.yaml").unwrap();
354    /// manager.load_from_config_file("config/database.properties").unwrap();
355    /// manager.load_from_config_file("config/server.toml").unwrap();
356    /// ```
357    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    /// Gets a configuration value by key
363    ///
364    /// # Arguments
365    ///
366    /// * `key` - The configuration key (e.g., "database.url")
367    ///
368    /// # Returns
369    ///
370    /// The configuration value if found, `None` otherwise
371    ///
372    /// # Examples
373    ///
374    /// ```rust
375    /// use verdure_context::{ConfigManager, ConfigSource};
376    /// use std::collections::HashMap;
377    ///
378    /// let mut manager = ConfigManager::new();
379    /// let mut props = HashMap::new();
380    /// props.insert("test.key".to_string(), "test.value".to_string());
381    ///
382    /// manager.add_source(ConfigSource::Properties(props)).unwrap();
383    ///
384    /// let value = manager.get("test.key");
385    /// assert!(value.is_some());
386    /// ```
387    /// Gets a configuration value
388    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    /// Internal method to compute and cache configuration values
397    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    /// Gets a configuration value as a string
410    ///
411    /// # Arguments
412    ///
413    /// * `key` - The configuration key
414    ///
415    /// # Returns
416    ///
417    /// The configuration value as a string
418    ///
419    /// # Errors
420    ///
421    /// Returns an error if the key is not found or cannot be converted to a string
422    ///
423    /// # Examples
424    ///
425    /// ```rust
426    /// use verdure_context::{ConfigManager, ConfigSource};
427    /// use std::collections::HashMap;
428    ///
429    /// let mut manager = ConfigManager::new();
430    /// let mut props = HashMap::new();
431    /// props.insert("app.name".to_string(), "MyApp".to_string());
432    ///
433    /// manager.add_source(ConfigSource::Properties(props)).unwrap();
434    ///
435    /// assert_eq!(manager.get_string("app.name").unwrap(), "MyApp");
436    /// ```
437    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    /// Gets a configuration value as an integer
444    ///
445    /// # Arguments
446    ///
447    /// * `key` - The configuration key
448    ///
449    /// # Returns
450    ///
451    /// The configuration value as an integer
452    ///
453    /// # Errors
454    ///
455    /// Returns an error if the key is not found or cannot be converted to an integer
456    ///
457    /// # Examples
458    ///
459    /// ```rust
460    /// use verdure_context::{ConfigManager, ConfigSource};
461    /// use std::collections::HashMap;
462    ///
463    /// let mut manager = ConfigManager::new();
464    /// let mut props = HashMap::new();
465    /// props.insert("app.port".to_string(), "8080".to_string());
466    ///
467    /// manager.add_source(ConfigSource::Properties(props)).unwrap();
468    ///
469    /// assert_eq!(manager.get_integer("app.port").unwrap(), 8080);
470    /// ```
471    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    /// Gets a configuration value as a float
478    ///
479    /// # Arguments
480    ///
481    /// * `key` - The configuration key
482    ///
483    /// # Returns
484    ///
485    /// The configuration value as a float
486    ///
487    /// # Errors
488    ///
489    /// Returns an error if the key is not found or cannot be converted to a float
490    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    /// Gets a configuration value as a boolean
497    ///
498    /// # Arguments
499    ///
500    /// * `key` - The configuration key
501    ///
502    /// # Returns
503    ///
504    /// The configuration value as a boolean
505    ///
506    /// # Errors
507    ///
508    /// Returns an error if the key is not found or cannot be converted to a boolean
509    ///
510    /// # Examples
511    ///
512    /// ```rust
513    /// use verdure_context::{ConfigManager, ConfigSource};
514    /// use std::collections::HashMap;
515    ///
516    /// let mut manager = ConfigManager::new();
517    /// let mut props = HashMap::new();
518    /// props.insert("app.debug".to_string(), "true".to_string());
519    ///
520    /// manager.add_source(ConfigSource::Properties(props)).unwrap();
521    ///
522    /// assert_eq!(manager.get_boolean("app.debug").unwrap(), true);
523    /// ```
524    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    /// Gets a configuration value with a default fallback
531    ///
532    /// # Arguments
533    ///
534    /// * `key` - The configuration key
535    /// * `default` - The default value to return if key is not found
536    ///
537    /// # Examples
538    ///
539    /// ```rust
540    /// use verdure_context::ConfigManager;
541    ///
542    /// let manager = ConfigManager::new();
543    ///
544    /// assert_eq!(manager.get_string_or_default("missing.key", "default"), "default");
545    /// ```
546    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    /// Gets an integer configuration value with a default fallback
551    ///
552    /// # Arguments
553    ///
554    /// * `key` - The configuration key
555    /// * `default` - The default value to return if key is not found
556    pub fn get_integer_or_default(&self, key: &str, default: i64) -> i64 {
557        self.get_integer(key).unwrap_or(default)
558    }
559
560    /// Gets a boolean configuration value with a default fallback
561    ///
562    /// # Arguments
563    ///
564    /// * `key` - The configuration key
565    /// * `default` - The default value to return if key is not found
566    pub fn get_boolean_or_default(&self, key: &str, default: bool) -> bool {
567        self.get_boolean(key).unwrap_or(default)
568    }
569
570    /// Sets a runtime configuration value
571    pub fn set(&self, key: &str, value: ConfigValue) {
572        self.cache.insert(key.to_string(), value);
573    }
574
575    /// Gets the number of configuration sources
576    ///
577    /// # Returns
578    ///
579    /// The total number of configuration sources
580    pub fn sources_count(&self) -> usize {
581        self.sources.read().len()
582    }
583
584    /// Invalidates the configuration cache
585    pub fn invalidate_cache(&self) {
586        self.cache.clear();
587        self.dirty_keys.clear();
588    }
589    
590    /// Invalidates specific cache keys
591    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    // Helper method to get value from a specific source
599    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                // Convert key to environment variable format (e.g., "app.port" -> "APP_PORT")
606                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, // TODO: Implement other source types
622        }
623    }
624
625    // Helper method to load configuration from file
626    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    // Helper method to auto-detect file format and load configuration
647    fn load_file_config_auto_detect(&self, path: &str) -> Option<HashMap<String, String>> {
648        let path_lower = path.to_lowercase();
649
650        // Try to detect format by extension first
651        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 extension doesn't match known formats, try parsing in order: TOML, YAML, Properties
660        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    // Helper method to convert YAML value to flat configuration map
672    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                                // Recursively process nested mappings
692                                let nested_map = self.yaml_value_to_config_map(val, &full_key)?;
693                                map.extend(nested_map);
694                            }
695                            _ => {
696                                // Convert primitive values to strings
697                                map.insert(full_key, self.yaml_value_to_string(val));
698                            }
699                        }
700                    }
701                }
702            }
703            _ => {
704                // For non-mapping values, use the prefix as the key
705                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    // Helper method to convert YAML value to string
715    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                // Convert array to comma-separated string
722                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    // Helper method to parse Properties format
733    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            // Skip empty lines and comments
740            if line.is_empty() || line.starts_with('#') || line.starts_with('!') {
741                continue;
742            }
743
744            // Find the first '=' or ':' separator
745            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                // Handle escaped characters and line continuations
750                let processed_value = self.process_properties_value(&value);
751
752                map.insert(key, processed_value);
753            }
754        }
755
756        Ok(map)
757    }
758
759    // Helper method to process Properties file values (handle escaping, etc.)
760    fn process_properties_value(&self, value: &str) -> String {
761        // Basic processing - handle common escape sequences
762        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                            // Recursively process nested tables
787                            let nested_map = self.toml_value_to_config_map(val, &full_key)?;
788                            map.extend(nested_map);
789                        }
790                        _ => {
791                            // Convert primitive values to strings
792                            map.insert(full_key, self.toml_value_to_string(val));
793                        }
794                    }
795                }
796            }
797            _ => {
798                // For non-table values, use the prefix as the key
799                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    // Helper method to convert TOML value to string
809    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                // Convert array to comma-separated string
817                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        // String conversion
840        let value = ConfigValue::String("hello".to_string());
841        assert_eq!(value.as_string(), Some("hello".to_string()));
842
843        // Integer conversion
844        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        // Boolean conversion
850        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        // String to boolean conversion
855        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        // String to integer conversion
862        let value = ConfigValue::String("123".to_string());
863        assert_eq!(value.as_integer(), Some(123));
864
865        // Invalid conversions
866        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        // Test missing key
894        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        // Add first source
914        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        // Add second source with overlapping key
922        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        // Second source should take precedence
930        assert_eq!(manager.get_string("app.name").unwrap(), "App2");
931        assert_eq!(manager.get_string("app.version").unwrap(), "1.0"); // Only in first source
932        assert_eq!(manager.get_string("app.port").unwrap(), "8080"); // Only in second source
933    }
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        // First access should populate cache
944        assert_eq!(manager.get_string("test.key").unwrap(), "test.value");
945
946        // Second access should use cache
947        assert_eq!(manager.get_string("test.key").unwrap(), "test.value");
948
949        // Manual cache update
950        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        // Access to populate cache
966        assert_eq!(manager.get_string("test.key").unwrap(), "test.value");
967
968        // Invalidate cache
969        manager.invalidate_cache();
970
971        // Should still work (reloaded from source)
972        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        // Test different source types
1063        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        // Add properties source
1076        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        // Add higher precedence properties (should override)
1082        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        // Higher precedence source should win
1090        assert_eq!(manager.get_string("app.name").unwrap(), "OverrideApp");
1091        assert_eq!(manager.get_string("app.version").unwrap(), "1.0"); // Only in first source
1092        assert_eq!(manager.get_string("app.env").unwrap(), "test"); // Only in second source
1093    }
1094}