sklears_utils/
config.rs

1//! Configuration Management Utilities
2//!
3//! This module provides comprehensive configuration management utilities for ML applications,
4//! including file parsing, environment variables, command-line arguments, validation, and hot-reloading.
5
6use crate::UtilsError;
7use serde::{Deserialize, Serialize};
8use serde_json;
9use std::collections::HashMap;
10use std::env;
11use std::fs;
12use std::path::{Path, PathBuf};
13use std::sync::{Arc, RwLock};
14use std::time::{Duration, SystemTime};
15
16/// Configuration value types
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18#[serde(untagged)]
19pub enum ConfigValue {
20    String(String),
21    Integer(i64),
22    Float(f64),
23    Boolean(bool),
24    Array(Vec<ConfigValue>),
25    Object(HashMap<String, ConfigValue>),
26    Null,
27}
28
29impl ConfigValue {
30    /// Convert to string
31    pub fn as_string(&self) -> Option<String> {
32        match self {
33            ConfigValue::String(s) => Some(s.clone()),
34            ConfigValue::Integer(i) => Some(i.to_string()),
35            ConfigValue::Float(f) => Some(f.to_string()),
36            ConfigValue::Boolean(b) => Some(b.to_string()),
37            _ => None,
38        }
39    }
40
41    /// Convert to integer
42    pub fn as_i64(&self) -> Option<i64> {
43        match self {
44            ConfigValue::Integer(i) => Some(*i),
45            ConfigValue::Float(f) => Some(*f as i64),
46            ConfigValue::String(s) => s.parse().ok(),
47            _ => None,
48        }
49    }
50
51    /// Convert to float
52    pub fn as_f64(&self) -> Option<f64> {
53        match self {
54            ConfigValue::Float(f) => Some(*f),
55            ConfigValue::Integer(i) => Some(*i as f64),
56            ConfigValue::String(s) => s.parse().ok(),
57            _ => None,
58        }
59    }
60
61    /// Convert to boolean
62    pub fn as_bool(&self) -> Option<bool> {
63        match self {
64            ConfigValue::Boolean(b) => Some(*b),
65            ConfigValue::String(s) => match s.to_lowercase().as_str() {
66                "true" | "yes" | "on" | "1" => Some(true),
67                "false" | "no" | "off" | "0" => Some(false),
68                _ => None,
69            },
70            ConfigValue::Integer(i) => Some(*i != 0),
71            _ => None,
72        }
73    }
74
75    /// Convert to array
76    pub fn as_array(&self) -> Option<&Vec<ConfigValue>> {
77        match self {
78            ConfigValue::Array(arr) => Some(arr),
79            _ => None,
80        }
81    }
82
83    /// Convert to object
84    pub fn as_object(&self) -> Option<&HashMap<String, ConfigValue>> {
85        match self {
86            ConfigValue::Object(obj) => Some(obj),
87            _ => None,
88        }
89    }
90
91    /// Check if value is null
92    pub fn is_null(&self) -> bool {
93        matches!(self, ConfigValue::Null)
94    }
95}
96
97impl From<String> for ConfigValue {
98    fn from(s: String) -> Self {
99        ConfigValue::String(s)
100    }
101}
102
103impl From<&str> for ConfigValue {
104    fn from(s: &str) -> Self {
105        ConfigValue::String(s.to_string())
106    }
107}
108
109impl From<i64> for ConfigValue {
110    fn from(i: i64) -> Self {
111        ConfigValue::Integer(i)
112    }
113}
114
115impl From<f64> for ConfigValue {
116    fn from(f: f64) -> Self {
117        ConfigValue::Float(f)
118    }
119}
120
121impl From<bool> for ConfigValue {
122    fn from(b: bool) -> Self {
123        ConfigValue::Boolean(b)
124    }
125}
126
127/// Configuration manager with hierarchical support
128#[derive(Debug, Clone)]
129pub struct Config {
130    data: HashMap<String, ConfigValue>,
131    sources: Vec<ConfigSource>,
132    metadata: ConfigMetadata,
133}
134
135#[derive(Debug, Clone)]
136pub struct ConfigMetadata {
137    pub loaded_at: SystemTime,
138    pub source_files: Vec<PathBuf>,
139    pub env_vars_used: Vec<String>,
140}
141
142#[derive(Debug, Clone)]
143pub enum ConfigSource {
144    File(PathBuf),
145    Environment,
146    CommandLine,
147    Default,
148}
149
150impl Default for Config {
151    fn default() -> Self {
152        Self::new()
153    }
154}
155
156impl Config {
157    /// Create a new empty configuration
158    pub fn new() -> Self {
159        Self {
160            data: HashMap::new(),
161            sources: Vec::new(),
162            metadata: ConfigMetadata {
163                loaded_at: SystemTime::now(),
164                source_files: Vec::new(),
165                env_vars_used: Vec::new(),
166            },
167        }
168    }
169
170    /// Load configuration from a file
171    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, UtilsError> {
172        let path = path.as_ref();
173        let content = fs::read_to_string(path).map_err(|e| {
174            UtilsError::InvalidParameter(format!("Failed to read config file: {e}"))
175        })?;
176
177        let mut config = Self::new();
178        config.sources.push(ConfigSource::File(path.to_path_buf()));
179        config.metadata.source_files.push(path.to_path_buf());
180
181        match path.extension().and_then(|ext| ext.to_str()) {
182            Some("json") => config.load_json(&content)?,
183            Some("toml") => config.load_toml(&content)?,
184            Some("yaml") | Some("yml") => config.load_yaml(&content)?,
185            _ => {
186                return Err(UtilsError::InvalidParameter(
187                    "Unsupported config file format".to_string(),
188                ))
189            }
190        }
191
192        Ok(config)
193    }
194
195    /// Load JSON configuration
196    fn load_json(&mut self, content: &str) -> Result<(), UtilsError> {
197        let json_value: serde_json::Value = serde_json::from_str(content)
198            .map_err(|e| UtilsError::InvalidParameter(format!("Invalid JSON: {e}")))?;
199
200        self.data = Self::json_to_config_map(json_value);
201        Ok(())
202    }
203
204    /// Load TOML configuration (simplified - would need toml crate in practice)
205    fn load_toml(&mut self, _content: &str) -> Result<(), UtilsError> {
206        // This is a placeholder - in practice you'd use the toml crate
207        Err(UtilsError::InvalidParameter(
208            "TOML support not implemented".to_string(),
209        ))
210    }
211
212    /// Load YAML configuration (simplified - would need yaml crate in practice)
213    fn load_yaml(&mut self, _content: &str) -> Result<(), UtilsError> {
214        // This is a placeholder - in practice you'd use the yaml crate
215        Err(UtilsError::InvalidParameter(
216            "YAML support not implemented".to_string(),
217        ))
218    }
219
220    /// Convert JSON value to config map
221    fn json_to_config_map(json: serde_json::Value) -> HashMap<String, ConfigValue> {
222        match json {
223            serde_json::Value::Object(map) => map
224                .into_iter()
225                .map(|(k, v)| (k, Self::json_value_to_config_value(v)))
226                .collect(),
227            _ => HashMap::new(),
228        }
229    }
230
231    /// Convert JSON value to config value
232    fn json_value_to_config_value(json: serde_json::Value) -> ConfigValue {
233        match json {
234            serde_json::Value::String(s) => ConfigValue::String(s),
235            serde_json::Value::Number(n) => {
236                if let Some(i) = n.as_i64() {
237                    ConfigValue::Integer(i)
238                } else if let Some(f) = n.as_f64() {
239                    ConfigValue::Float(f)
240                } else {
241                    ConfigValue::Null
242                }
243            }
244            serde_json::Value::Bool(b) => ConfigValue::Boolean(b),
245            serde_json::Value::Array(arr) => ConfigValue::Array(
246                arr.into_iter()
247                    .map(Self::json_value_to_config_value)
248                    .collect(),
249            ),
250            serde_json::Value::Object(obj) => ConfigValue::Object(
251                obj.into_iter()
252                    .map(|(k, v)| (k, Self::json_value_to_config_value(v)))
253                    .collect(),
254            ),
255            serde_json::Value::Null => ConfigValue::Null,
256        }
257    }
258
259    /// Load environment variables with prefix
260    pub fn load_environment(&mut self, prefix: &str) -> Result<(), UtilsError> {
261        self.sources.push(ConfigSource::Environment);
262
263        for (key, value) in env::vars() {
264            if let Some(stripped) = key.strip_prefix(prefix) {
265                let config_key = stripped.to_lowercase().replace('_', ".");
266                self.data
267                    .insert(config_key, ConfigValue::String(value.clone()));
268                self.metadata.env_vars_used.push(key);
269            }
270        }
271
272        Ok(())
273    }
274
275    /// Get configuration value by key (supports dot notation)
276    pub fn get(&self, key: &str) -> Option<&ConfigValue> {
277        if key.contains('.') {
278            self.get_nested(key)
279        } else {
280            self.data.get(key)
281        }
282    }
283
284    /// Get nested configuration value
285    fn get_nested(&self, key: &str) -> Option<&ConfigValue> {
286        let parts: Vec<&str> = key.split('.').collect();
287        let mut current = self.data.get(parts[0])?;
288
289        for part in &parts[1..] {
290            if let ConfigValue::Object(obj) = current {
291                current = obj.get(*part)?;
292            } else {
293                return None;
294            }
295        }
296
297        Some(current)
298    }
299
300    /// Set configuration value
301    pub fn set(&mut self, key: &str, value: ConfigValue) {
302        if key.contains('.') {
303            self.set_nested(key, value);
304        } else {
305            self.data.insert(key.to_string(), value);
306        }
307    }
308
309    /// Set nested configuration value
310    fn set_nested(&mut self, key: &str, value: ConfigValue) {
311        let parts: Vec<&str> = key.split('.').collect();
312        if parts.is_empty() {
313            return;
314        }
315
316        if parts.len() == 1 {
317            self.data.insert(parts[0].to_string(), value);
318            return;
319        }
320
321        // Use a static recursive approach to avoid borrow checker issues
322        Self::set_nested_recursive(&mut self.data, &parts, 0, value);
323    }
324
325    /// Recursive helper for setting nested values
326    fn set_nested_recursive(
327        current: &mut HashMap<String, ConfigValue>,
328        parts: &[&str],
329        index: usize,
330        value: ConfigValue,
331    ) {
332        if index == parts.len() - 1 {
333            // Insert the final value
334            current.insert(parts[index].to_string(), value);
335            return;
336        }
337
338        let part = parts[index].to_string();
339
340        // Ensure the entry exists and is an object
341        let entry = current
342            .entry(part)
343            .or_insert_with(|| ConfigValue::Object(HashMap::new()));
344
345        match entry {
346            ConfigValue::Object(ref mut obj) => {
347                Self::set_nested_recursive(obj, parts, index + 1, value);
348            }
349            _ => {
350                // Convert non-object to object
351                *entry = ConfigValue::Object(HashMap::new());
352                if let ConfigValue::Object(ref mut obj) = entry {
353                    Self::set_nested_recursive(obj, parts, index + 1, value);
354                }
355            }
356        }
357    }
358
359    /// Get string value with default
360    pub fn get_string(&self, key: &str, default: &str) -> String {
361        self.get(key)
362            .and_then(|v| v.as_string())
363            .unwrap_or_else(|| default.to_string())
364    }
365
366    /// Get integer value with default
367    pub fn get_i64(&self, key: &str, default: i64) -> i64 {
368        self.get(key).and_then(|v| v.as_i64()).unwrap_or(default)
369    }
370
371    /// Get float value with default
372    pub fn get_f64(&self, key: &str, default: f64) -> f64 {
373        self.get(key).and_then(|v| v.as_f64()).unwrap_or(default)
374    }
375
376    /// Get boolean value with default
377    pub fn get_bool(&self, key: &str, default: bool) -> bool {
378        self.get(key).and_then(|v| v.as_bool()).unwrap_or(default)
379    }
380
381    /// Merge another configuration
382    pub fn merge(&mut self, other: &Config) {
383        for (key, value) in &other.data {
384            self.data.insert(key.clone(), value.clone());
385        }
386        self.sources.extend(other.sources.clone());
387    }
388
389    /// Get all keys
390    pub fn keys(&self) -> Vec<String> {
391        let mut keys = Vec::new();
392        self.collect_keys("", &self.data, &mut keys);
393        keys
394    }
395
396    /// Recursively collect all keys
397    #[allow(clippy::only_used_in_recursion)]
398    fn collect_keys(
399        &self,
400        prefix: &str,
401        data: &HashMap<String, ConfigValue>,
402        keys: &mut Vec<String>,
403    ) {
404        for (key, value) in data {
405            let full_key = if prefix.is_empty() {
406                key.clone()
407            } else {
408                format!("{prefix}.{key}")
409            };
410
411            keys.push(full_key.clone());
412
413            if let ConfigValue::Object(obj) = value {
414                self.collect_keys(&full_key, obj, keys);
415            }
416        }
417    }
418
419    /// Serialize configuration to JSON
420    pub fn to_json(&self) -> Result<String, UtilsError> {
421        let json_value = self.config_map_to_json(&self.data);
422        serde_json::to_string_pretty(&json_value)
423            .map_err(|e| UtilsError::InvalidParameter(format!("Failed to serialize config: {e}")))
424    }
425
426    /// Convert config map to JSON value
427    fn config_map_to_json(&self, data: &HashMap<String, ConfigValue>) -> serde_json::Value {
428        let mut map = serde_json::Map::new();
429
430        for (key, value) in data {
431            map.insert(key.clone(), self.config_value_to_json(value));
432        }
433
434        serde_json::Value::Object(map)
435    }
436
437    /// Convert config value to JSON value
438    #[allow(clippy::only_used_in_recursion)]
439    fn config_value_to_json(&self, value: &ConfigValue) -> serde_json::Value {
440        match value {
441            ConfigValue::String(s) => serde_json::Value::String(s.clone()),
442            ConfigValue::Integer(i) => serde_json::Value::Number((*i).into()),
443            ConfigValue::Float(f) => serde_json::Value::Number(
444                serde_json::Number::from_f64(*f).unwrap_or_else(|| 0.into()),
445            ),
446            ConfigValue::Boolean(b) => serde_json::Value::Bool(*b),
447            ConfigValue::Array(arr) => {
448                serde_json::Value::Array(arr.iter().map(|v| self.config_value_to_json(v)).collect())
449            }
450            ConfigValue::Object(obj) => {
451                let mut map = serde_json::Map::new();
452                for (k, v) in obj {
453                    map.insert(k.clone(), self.config_value_to_json(v));
454                }
455                serde_json::Value::Object(map)
456            }
457            ConfigValue::Null => serde_json::Value::Null,
458        }
459    }
460}
461
462/// Configuration validation utilities
463pub struct ConfigValidator;
464
465impl ConfigValidator {
466    /// Validate required keys exist
467    pub fn validate_required_keys(
468        config: &Config,
469        required_keys: &[&str],
470    ) -> Result<(), UtilsError> {
471        for key in required_keys {
472            if config.get(key).is_none() {
473                return Err(UtilsError::InvalidParameter(format!(
474                    "Required configuration key '{key}' is missing"
475                )));
476            }
477        }
478        Ok(())
479    }
480
481    /// Validate value types
482    pub fn validate_types(
483        config: &Config,
484        type_specs: &HashMap<&str, &str>,
485    ) -> Result<(), UtilsError> {
486        for (key, expected_type) in type_specs {
487            if let Some(value) = config.get(key) {
488                let valid = match *expected_type {
489                    "string" => matches!(value, ConfigValue::String(_)),
490                    "integer" => matches!(value, ConfigValue::Integer(_)),
491                    "float" => {
492                        matches!(value, ConfigValue::Float(_))
493                            || matches!(value, ConfigValue::Integer(_))
494                    }
495                    "boolean" => matches!(value, ConfigValue::Boolean(_)),
496                    "array" => matches!(value, ConfigValue::Array(_)),
497                    "object" => matches!(value, ConfigValue::Object(_)),
498                    _ => false,
499                };
500
501                if !valid {
502                    return Err(UtilsError::InvalidParameter(format!(
503                        "Configuration key '{key}' has wrong type, expected {expected_type}"
504                    )));
505                }
506            }
507        }
508        Ok(())
509    }
510
511    /// Validate value ranges for numeric types
512    pub fn validate_ranges(
513        config: &Config,
514        range_specs: &HashMap<&str, (f64, f64)>,
515    ) -> Result<(), UtilsError> {
516        for (key, (min_val, max_val)) in range_specs {
517            if let Some(value) = config.get(key) {
518                let numeric_value = match value {
519                    ConfigValue::Integer(i) => Some(*i as f64),
520                    ConfigValue::Float(f) => Some(*f),
521                    _ => None,
522                };
523
524                if let Some(val) = numeric_value {
525                    if val < *min_val || val > *max_val {
526                        return Err(UtilsError::InvalidParameter(format!(
527                            "Configuration key '{key}' value {val} is outside valid range [{min_val}, {max_val}]"
528                        )));
529                    }
530                }
531            }
532        }
533        Ok(())
534    }
535
536    /// Validate configuration using a custom validator function
537    pub fn validate_custom<F>(config: &Config, validator: F) -> Result<(), UtilsError>
538    where
539        F: Fn(&Config) -> Result<(), String>,
540    {
541        validator(config).map_err(UtilsError::InvalidParameter)
542    }
543}
544
545/// Hot-reloading configuration manager
546pub struct HotReloadConfig {
547    config: Arc<RwLock<Config>>,
548    file_path: PathBuf,
549    last_modified: SystemTime,
550    check_interval: Duration,
551}
552
553impl HotReloadConfig {
554    /// Create a new hot-reload configuration manager
555    pub fn new<P: AsRef<Path>>(file_path: P, check_interval: Duration) -> Result<Self, UtilsError> {
556        let file_path = file_path.as_ref().to_path_buf();
557        let config = Config::from_file(&file_path)?;
558
559        let last_modified = fs::metadata(&file_path)
560            .and_then(|m| m.modified())
561            .map_err(|e| {
562                UtilsError::InvalidParameter(format!("Failed to get file metadata: {e}"))
563            })?;
564
565        Ok(Self {
566            config: Arc::new(RwLock::new(config)),
567            file_path,
568            last_modified,
569            check_interval,
570        })
571    }
572
573    /// Get the current configuration
574    pub fn get_config(&self) -> Arc<RwLock<Config>> {
575        self.config.clone()
576    }
577
578    /// Check for file updates and reload if necessary
579    pub fn check_and_reload(&mut self) -> Result<bool, UtilsError> {
580        let current_modified = fs::metadata(&self.file_path)
581            .and_then(|m| m.modified())
582            .map_err(|e| {
583                UtilsError::InvalidParameter(format!("Failed to get file metadata: {e}"))
584            })?;
585
586        if current_modified > self.last_modified {
587            let new_config = Config::from_file(&self.file_path)?;
588
589            {
590                let mut config = self.config.write().unwrap();
591                *config = new_config;
592            }
593
594            self.last_modified = current_modified;
595            Ok(true)
596        } else {
597            Ok(false)
598        }
599    }
600
601    /// Start automatic reloading in a background thread
602    pub fn start_auto_reload(mut self) -> std::thread::JoinHandle<()> {
603        std::thread::spawn(move || loop {
604            std::thread::sleep(self.check_interval);
605
606            if let Err(e) = self.check_and_reload() {
607                eprintln!("Error reloading config: {e}");
608            }
609        })
610    }
611}
612
613/// Command-line argument parser
614pub struct ArgParser {
615    args: Vec<String>,
616    config: Config,
617}
618
619impl Default for ArgParser {
620    fn default() -> Self {
621        Self::new()
622    }
623}
624
625impl ArgParser {
626    /// Create a new argument parser
627    pub fn new() -> Self {
628        let args: Vec<String> = env::args().collect();
629        Self {
630            args,
631            config: Config::new(),
632        }
633    }
634
635    /// Parse command-line arguments into configuration
636    pub fn parse(&mut self) -> Result<(), UtilsError> {
637        let mut i = 1; // Skip program name
638
639        while i < self.args.len() {
640            let arg = &self.args[i];
641
642            if let Some(stripped) = arg.strip_prefix("--") {
643                // Long option
644                let key = stripped;
645
646                if let Some(eq_pos) = key.find('=') {
647                    // --key=value format
648                    let (k, v) = key.split_at(eq_pos);
649                    let value = &v[1..]; // Skip the '='
650                    self.config.set(k, self.parse_value(value));
651                } else if i + 1 < self.args.len() && !self.args[i + 1].starts_with('-') {
652                    // --key value format
653                    i += 1;
654                    let value = &self.args[i];
655                    self.config.set(key, self.parse_value(value));
656                } else {
657                    // Boolean flag
658                    self.config.set(key, ConfigValue::Boolean(true));
659                }
660            } else if arg.starts_with('-') && arg.len() == 2 {
661                // Short option
662                let key = &arg[1..];
663
664                if i + 1 < self.args.len() && !self.args[i + 1].starts_with('-') {
665                    i += 1;
666                    let value = &self.args[i];
667                    self.config.set(key, self.parse_value(value));
668                } else {
669                    // Boolean flag
670                    self.config.set(key, ConfigValue::Boolean(true));
671                }
672            }
673
674            i += 1;
675        }
676
677        Ok(())
678    }
679
680    /// Parse string value to appropriate type
681    #[allow(clippy::only_used_in_recursion)]
682    fn parse_value(&self, value: &str) -> ConfigValue {
683        // Try to parse as different types
684        if let Ok(b) = value.parse::<bool>() {
685            ConfigValue::Boolean(b)
686        } else if let Ok(i) = value.parse::<i64>() {
687            ConfigValue::Integer(i)
688        } else if let Ok(f) = value.parse::<f64>() {
689            ConfigValue::Float(f)
690        } else if value.starts_with('[') && value.ends_with(']') {
691            // Simple array parsing
692            let inner = &value[1..value.len() - 1];
693            let items: Vec<ConfigValue> = inner
694                .split(',')
695                .map(|s| self.parse_value(s.trim()))
696                .collect();
697            ConfigValue::Array(items)
698        } else {
699            ConfigValue::String(value.to_string())
700        }
701    }
702
703    /// Get the parsed configuration
704    pub fn get_config(self) -> Config {
705        self.config
706    }
707}
708
709/// Configuration builder for fluent API
710pub struct ConfigBuilder {
711    config: Config,
712}
713
714impl Default for ConfigBuilder {
715    fn default() -> Self {
716        Self::new()
717    }
718}
719
720impl ConfigBuilder {
721    /// Create a new configuration builder
722    pub fn new() -> Self {
723        Self {
724            config: Config::new(),
725        }
726    }
727
728    /// Add configuration from file
729    pub fn add_file<P: AsRef<Path>>(mut self, path: P) -> Result<Self, UtilsError> {
730        let file_config = Config::from_file(path)?;
731        self.config.merge(&file_config);
732        Ok(self)
733    }
734
735    /// Add environment variables with prefix
736    pub fn add_env(mut self, prefix: &str) -> Result<Self, UtilsError> {
737        self.config.load_environment(prefix)?;
738        Ok(self)
739    }
740
741    /// Add command-line arguments
742    pub fn add_args(mut self) -> Result<Self, UtilsError> {
743        let mut parser = ArgParser::new();
744        parser.parse()?;
745        self.config.merge(&parser.get_config());
746        Ok(self)
747    }
748
749    /// Set a default value
750    pub fn set_default(mut self, key: &str, value: ConfigValue) -> Self {
751        if self.config.get(key).is_none() {
752            self.config.set(key, value);
753        }
754        self
755    }
756
757    /// Validate the configuration
758    pub fn validate<F>(self, validator: F) -> Result<Self, UtilsError>
759    where
760        F: Fn(&Config) -> Result<(), String>,
761    {
762        ConfigValidator::validate_custom(&self.config, validator)?;
763        Ok(self)
764    }
765
766    /// Build the final configuration
767    pub fn build(self) -> Config {
768        self.config
769    }
770}
771
772#[allow(non_snake_case)]
773#[cfg(test)]
774mod tests {
775    use super::*;
776    use std::io::Write;
777    use tempfile::NamedTempFile;
778
779    #[test]
780    fn test_config_value_conversions() {
781        let string_val = ConfigValue::String("test".to_string());
782        assert_eq!(string_val.as_string(), Some("test".to_string()));
783
784        let int_val = ConfigValue::Integer(42);
785        assert_eq!(int_val.as_i64(), Some(42));
786        assert_eq!(int_val.as_f64(), Some(42.0));
787
788        let bool_val = ConfigValue::Boolean(true);
789        assert_eq!(bool_val.as_bool(), Some(true));
790
791        let float_val = ConfigValue::Float(std::f64::consts::PI);
792        assert_eq!(float_val.as_f64(), Some(std::f64::consts::PI));
793    }
794
795    #[test]
796    fn test_config_get_set() {
797        let mut config = Config::new();
798
799        config.set("test.key", ConfigValue::String("value".to_string()));
800        assert_eq!(config.get_string("test.key", "default"), "value");
801
802        config.set("number", ConfigValue::Integer(42));
803        assert_eq!(config.get_i64("number", 0), 42);
804
805        config.set("flag", ConfigValue::Boolean(true));
806        assert!(config.get_bool("flag", false));
807    }
808
809    #[test]
810    fn test_json_config_loading() {
811        let json_content = r#"
812        {
813            "database": {
814                "host": "localhost",
815                "port": 5432,
816                "name": "test_db"
817            },
818            "debug": true,
819            "timeout": 30.5
820        }
821        "#;
822
823        let mut temp_file = NamedTempFile::with_suffix(".json").unwrap();
824        temp_file.write_all(json_content.as_bytes()).unwrap();
825
826        let config = Config::from_file(temp_file.path()).unwrap();
827
828        assert_eq!(config.get_string("database.host", ""), "localhost");
829        assert_eq!(config.get_i64("database.port", 0), 5432);
830        assert_eq!(config.get_string("database.name", ""), "test_db");
831        assert!(config.get_bool("debug", false));
832        assert_eq!(config.get_f64("timeout", 0.0), 30.5);
833    }
834
835    #[test]
836    fn test_config_validation() {
837        let mut config = Config::new();
838        config.set("required_key", ConfigValue::String("value".to_string()));
839        config.set("number_key", ConfigValue::Integer(50));
840
841        // Test required keys validation
842        let required_keys = vec!["required_key"];
843        assert!(ConfigValidator::validate_required_keys(&config, &required_keys).is_ok());
844
845        let missing_keys = vec!["missing_key"];
846        assert!(ConfigValidator::validate_required_keys(&config, &missing_keys).is_err());
847
848        // Test type validation
849        let mut type_specs = HashMap::new();
850        type_specs.insert("required_key", "string");
851        type_specs.insert("number_key", "integer");
852        assert!(ConfigValidator::validate_types(&config, &type_specs).is_ok());
853
854        type_specs.insert("number_key", "string");
855        assert!(ConfigValidator::validate_types(&config, &type_specs).is_err());
856
857        // Test range validation
858        let mut range_specs = HashMap::new();
859        range_specs.insert("number_key", (0.0, 100.0));
860        assert!(ConfigValidator::validate_ranges(&config, &range_specs).is_ok());
861
862        range_specs.insert("number_key", (60.0, 100.0));
863        assert!(ConfigValidator::validate_ranges(&config, &range_specs).is_err());
864    }
865
866    #[test]
867    fn test_arg_parser() {
868        // Mock command line arguments
869        let args = vec![
870            "program".to_string(),
871            "--host=localhost".to_string(),
872            "--port".to_string(),
873            "8080".to_string(),
874            "--debug".to_string(),
875            "-v".to_string(),
876        ];
877
878        let mut parser = ArgParser {
879            args,
880            config: Config::new(),
881        };
882        parser.parse().unwrap();
883
884        let config = parser.get_config();
885        assert_eq!(config.get_string("host", ""), "localhost");
886        assert_eq!(config.get_i64("port", 0), 8080);
887        assert!(config.get_bool("debug", false));
888        assert!(config.get_bool("v", false));
889    }
890
891    #[test]
892    fn test_config_builder() {
893        let config = ConfigBuilder::new()
894            .set_default("host", ConfigValue::String("localhost".to_string()))
895            .set_default("port", ConfigValue::Integer(8080))
896            .set_default("debug", ConfigValue::Boolean(false))
897            .build();
898
899        assert_eq!(config.get_string("host", ""), "localhost");
900        assert_eq!(config.get_i64("port", 0), 8080);
901        assert!(!config.get_bool("debug", true));
902    }
903
904    #[test]
905    fn test_config_merge() {
906        let mut config1 = Config::new();
907        config1.set("key1", ConfigValue::String("value1".to_string()));
908        config1.set("key2", ConfigValue::Integer(42));
909
910        let mut config2 = Config::new();
911        config2.set("key2", ConfigValue::Integer(100)); // Override
912        config2.set("key3", ConfigValue::Boolean(true)); // New key
913
914        config1.merge(&config2);
915
916        assert_eq!(config1.get_string("key1", ""), "value1");
917        assert_eq!(config1.get_i64("key2", 0), 100); // Overridden
918        assert!(config1.get_bool("key3", false)); // New key
919    }
920}