dmsc/config/
mod.rs

1//! Copyright © 2025-2026 Wenze Wei. All Rights Reserved.
2//!
3//! This file is part of DMSC.
4//! The DMSC project belongs to the Dunimd Team.
5//!
6//! Licensed under the Apache License, Version 2.0 (the "License");
7//! You may not use this file except in compliance with the License.
8//! You may obtain a copy of the License at
9//!
10//!     http://www.apache.org/licenses/LICENSE-2.0
11//!
12//! Unless required by applicable law or agreed to in writing, software
13//! distributed under the License is distributed on an "AS IS" BASIS,
14//! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15//! See the License for the specific language governing permissions and
16//! limitations under the License.
17
18//! # Configuration Management
19//! 
20//! This module provides a comprehensive configuration management system for DMSC, supporting
21//! multiple configuration sources, hot reload, and flexible configuration access.
22//! 
23//! ## Key Components
24//! 
25//! - **DMSCConfig**: Basic configuration storage with typed access methods
26//! - **DMSCConfigManager**: Configuration manager that handles multiple sources and hot reload
27//! - **DMSCConfigSource**: Internal enum for different configuration source types
28//! 
29//! ## Design Principles
30//! 
31//! 1. **Multiple Sources**: Supports configuration from files (JSON, YAML, TOML) and environment variables
32//! 2. **Source Priority**: Environment variables override file configuration
33//! 3. **Typed Access**: Provides type-safe methods for accessing configuration values
34//! 4. **Flattened Structure**: All configuration is flattened into a single key-value store with dot notation
35//! 5. **Hot Reload Support**: Simplified hot reload implementation with full support planned for future
36//! 6. **Default Sources**: Automatically loads configuration from common locations
37//! 
38//! ## Usage
39//! 
40//! ```rust
41//! use dmsc::prelude::*;
42//! 
43//! fn example() -> DMSCResult<()> {
44//!     // Create a new config manager
45//!     let mut config_manager = DMSCConfigManager::new();
46//!     
47//!     // Add configuration sources
48//!     config_manager.add_file_source("config.yaml");
49//!     config_manager.add_environment_source();
50//!     
51//!     // Load configuration
52//!     config_manager.load()?;
53//!     
54//!     // Access configuration values
55//!     let config = config_manager.config();
56//!     let port = config.get_u64("server.port").unwrap_or(8080);
57//!     let debug = config.get_bool("app.debug").unwrap_or(false);
58//!     
59//!     Ok(())
60//! }
61//! ```
62
63use std::collections::HashMap;
64use std::fs;
65use std::path::{Path, PathBuf};
66use std::sync::{Arc, RwLock};
67use tokio::sync::RwLock as TokioRwLock;
68use tokio::task::JoinHandle;
69use yaml_rust::{YamlLoader, Yaml};
70
71#[cfg(feature = "config_hot_reload")]
72use notify::{RecommendedWatcher, RecursiveMode, Watcher};
73
74#[cfg(feature = "pyo3")]
75use crate::hooks::DMSCHookKind;
76#[cfg(feature = "pyo3")]
77use crate::core::DMSCServiceContext;
78
79/// Basic configuration storage with typed access methods.
80/// 
81/// This struct provides a simple key-value store for configuration values, with
82/// type-safe methods for accessing values as different types.
83#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
84#[derive(Clone)]
85pub struct DMSCConfig {
86    /// Internal storage for configuration values
87    values: HashMap<String, String>,
88}
89
90impl Default for DMSCConfig {
91    fn default() -> Self {
92        Self::new()
93    }
94}
95
96impl DMSCConfig {
97    /// Creates a new empty configuration.
98    /// 
99    /// Returns a new `DMSCConfig` instance with an empty key-value store.
100    pub fn new() -> Self {
101        DMSCConfig { values: HashMap::new() }
102    }
103
104    /// Sets a configuration value.
105    /// 
106    /// # Parameters
107    /// 
108    /// - `key`: The configuration key (typically using dot notation, e.g., "server.port")
109    /// - `value`: The configuration value as a string
110    pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
111        self.values.insert(key.into(), value.into());
112    }
113
114    /// Gets a configuration value as a string.
115    /// 
116    /// # Parameters
117    /// 
118    /// - `key`: The configuration key to look up
119    /// 
120    /// # Returns
121    /// 
122    /// An `Option<&String>` containing the value if it exists
123    pub fn get(&self, key: &str) -> Option<&String> {
124        self.values.get(key)
125    }
126
127    /// Gets a configuration value as a string slice.
128    /// 
129    /// # Parameters
130    /// 
131    /// - `key`: The configuration key to look up
132    /// 
133    /// # Returns
134    /// 
135    /// An `Option<&str>` containing the value if it exists
136    pub fn get_str(&self, key: &str) -> Option<&str> {
137        self.values.get(key).map(|s| s.as_str())
138    }
139
140    /// Gets a configuration value as a boolean.
141    /// 
142    /// Supports the following truthy values: "true", "1", "yes", "on"
143    /// Supports the following falsy values: "false", "0", "no", "off"
144    /// 
145    /// # Parameters
146    /// 
147    /// - `key`: The configuration key to look up
148    /// 
149    /// # Returns
150    /// 
151    /// An `Option<bool>` containing the parsed boolean value if the key exists and can be parsed
152    pub fn get_bool(&self, key: &str) -> Option<bool> {
153        self.values.get(key).and_then(|s| {
154            let v = s.trim().to_ascii_lowercase();
155            match v.as_str() {
156                "true" | "1" | "yes" | "on" => Some(true),
157                "false" | "0" | "no" | "off" => Some(false),
158                _ => None,
159            }
160        })
161    }
162
163    /// Gets a configuration value as a 64-bit signed integer.
164    /// 
165    /// # Parameters
166    /// 
167    /// - `key`: The configuration key to look up
168    /// 
169    /// # Returns
170    /// 
171    /// An `Option<i64>` containing the parsed integer value if the key exists and can be parsed
172    pub fn get_i64(&self, key: &str) -> Option<i64> {
173        self.values.get(key).and_then(|s| s.trim().parse::<i64>().ok())
174    }
175
176    /// Gets a configuration value as a 64-bit signed integer with bounds checking.
177    /// 
178    /// # Parameters
179    /// 
180    /// - `key`: The configuration key to look up
181    /// - `min`: Minimum allowed value
182    /// - `max`: Maximum allowed value
183    /// 
184    /// # Returns
185    /// 
186    /// An `Option<i64>` containing the parsed and validated integer value
187    pub fn get_i64_with_bounds(&self, key: &str, min: i64, max: i64) -> Option<i64> {
188        self.get_i64(key).filter(|&v| v >= min && v <= max)
189    }
190
191    /// Gets a configuration value as a 64-bit unsigned integer.
192    /// 
193    /// # Parameters
194    /// 
195    /// - `key`: The configuration key to look up
196    /// 
197    /// # Returns
198    /// 
199    /// An `Option<u64>` containing the parsed integer value if the key exists and can be parsed
200    pub fn get_u64(&self, key: &str) -> Option<u64> {
201        self.values.get(key).and_then(|s| s.trim().parse::<u64>().ok())
202    }
203
204    /// Gets a configuration value as a 64-bit unsigned integer with bounds checking.
205    /// 
206    /// # Parameters
207    /// 
208    /// - `key`: The configuration key to look up
209    /// - `min`: Minimum allowed value (default: 0)
210    /// - `max`: Maximum allowed value
211    /// 
212    /// # Returns
213    /// 
214    /// An `Option<u64>` containing the parsed and validated integer value
215    pub fn get_u64_with_bounds(&self, key: &str, min: u64, max: u64) -> Option<u64> {
216        self.get_u64(key).filter(|&v| v >= min && v <= max)
217    }
218
219    /// Gets a configuration value as a positive 64-bit unsigned integer (must be > 0).
220    /// 
221    /// # Parameters
222    /// 
223    /// - `key`: The configuration key to look up
224    /// 
225    /// # Returns
226    /// 
227    /// An `Option<u64>` containing the positive integer value
228    pub fn get_positive_u64(&self, key: &str) -> Option<u64> {
229        self.get_u64(key).filter(|&v| v > 0)
230    }
231
232    /// Gets a configuration value as a port number (1-65535).
233    /// 
234    /// # Parameters
235    /// 
236    /// - `key`: The configuration key to look up
237    /// 
238    /// # Returns
239    /// 
240    /// An `Option<u16>` containing the valid port number
241    pub fn get_port(&self, key: &str) -> Option<u16> {
242        self.get_u64_with_bounds(key, 1, 65535).map(|v| v as u16)
243    }
244
245    /// Gets a configuration value as a 32-bit floating point number.
246    /// 
247    /// # Parameters
248    /// 
249    /// - `key`: The configuration key to look up
250    /// 
251    /// # Returns
252    /// 
253    /// An `Option<f32>` containing the parsed float value if the key exists and can be parsed
254    pub fn get_f32(&self, key: &str) -> Option<f32> {
255        self.values.get(key).and_then(|s| s.trim().parse::<f32>().ok())
256    }
257
258    /// Gets a configuration value as a 32-bit floating point number with bounds checking.
259    /// 
260    /// # Parameters
261    /// 
262    /// - `key`: The configuration key to look up
263    /// - `min`: Minimum allowed value
264    /// - `max`: Maximum allowed value
265    /// 
266    /// # Returns
267    /// 
268    /// An `Option<f32>` containing the parsed and validated float value
269    pub fn get_f32_with_bounds(&self, key: &str, min: f32, max: f32) -> Option<f32> {
270        self.get_f32(key).filter(|&v| v >= min && v <= max)
271    }
272
273    /// Gets a configuration value as a percentage (0.0-100.0).
274    /// 
275    /// # Parameters
276    /// 
277    /// - `key`: The configuration key to look up
278    /// 
279    /// # Returns
280    /// 
281    /// An `Option<f32>` containing the percentage value
282    pub fn get_percentage(&self, key: &str) -> Option<f32> {
283        self.get_f32_with_bounds(key, 0.0, 100.0)
284    }
285
286    /// Gets a configuration value as a rate (0.0-1.0).
287    /// 
288    /// # Parameters
289    /// 
290    /// - `key`: The configuration key to look up
291    /// 
292    /// # Returns
293    /// 
294    /// An `Option<f32>` containing the rate value
295    pub fn get_rate(&self, key: &str) -> Option<f32> {
296        self.get_f32_with_bounds(key, 0.0, 1.0)
297    }
298
299    /// Merges another configuration into this one.
300    /// 
301    /// Values from the other configuration will override existing values with the same keys.
302    /// 
303    /// # Parameters
304    /// 
305    /// - `other`: The other configuration to merge into this one
306    pub fn merge(&mut self, other: &DMSCConfig) {
307        for (k, v) in &other.values {
308            self.values.insert(k.clone(), v.clone());
309        }
310    }
311
312    /// Clears all configuration values.
313    /// 
314    /// Removes all key-value pairs from the configuration.
315    pub fn clear(&mut self) {
316        self.values.clear();
317    }
318
319    pub fn get_or_default<T>(&self, key: &str, default: T) -> T 
320    where
321        T: std::str::FromStr,
322        T::Err: std::fmt::Debug,
323    {
324        self.values.get(key).and_then(|s| s.trim().parse::<T>().ok()).unwrap_or(default)
325    }
326
327    pub fn get_f64(&self, key: &str) -> Option<f64> {
328        self.values.get(key).and_then(|s| s.trim().parse::<f64>().ok())
329    }
330
331    pub fn get_usize(&self, key: &str) -> Option<usize> {
332        self.values.get(key).and_then(|s| s.trim().parse::<usize>().ok())
333    }
334
335    pub fn get_i32(&self, key: &str) -> Option<i32> {
336        self.values.get(key).and_then(|s| s.trim().parse::<i32>().ok())
337    }
338
339    pub fn get_u32(&self, key: &str) -> Option<u32> {
340        self.values.get(key).and_then(|s| s.trim().parse::<u32>().ok())
341    }
342
343    /// Gets a configuration value as a 32-bit unsigned integer with bounds checking.
344    /// 
345    /// # Parameters
346    /// 
347    /// - `key`: The configuration key to look up
348    /// - `min`: Minimum allowed value
349    /// - `max`: Maximum allowed value
350    /// 
351    /// # Returns
352    /// 
353    /// An `Option<u32>` containing the parsed and validated integer value
354    pub fn get_u32_with_bounds(&self, key: &str, min: u32, max: u32) -> Option<u32> {
355        self.get_u32(key).filter(|&v| v >= min && v <= max)
356    }
357
358    /// Gets a configuration value as a timeout value in seconds (1-86400).
359    /// 
360    /// # Parameters
361    /// 
362    /// - `key`: The configuration key to look up
363    /// 
364    /// # Returns
365    /// 
366    /// An `Option<u32>` containing the timeout in seconds
367    pub fn get_timeout_secs(&self, key: &str) -> Option<u32> {
368        self.get_u32_with_bounds(key, 1, 86400)
369    }
370
371    /// Gets a configuration value as a retry count (0-100).
372    /// 
373    /// # Parameters
374    /// 
375    /// - `key`: The configuration key to look up
376    /// 
377    /// # Returns
378    /// 
379    /// An `Option<u32>` containing the retry count
380    pub fn get_retry_count(&self, key: &str) -> Option<u32> {
381        self.get_u32_with_bounds(key, 0, 100)
382    }
383
384    pub fn keys(&self) -> Vec<&str> {
385        self.values.keys().map(|s| s.as_str()).collect()
386    }
387
388    pub fn all_values(&self) -> Vec<&str> {
389        self.values.values().map(|s| s.as_str()).collect()
390    }
391
392    pub fn has_key(&self, key: &str) -> bool {
393        self.values.contains_key(key)
394    }
395
396    pub fn count(&self) -> usize {
397        self.values.len()
398    }
399
400    #[cfg(feature = "pyo3")]
401    pub fn is_empty(&self) -> bool {
402        self.values.is_empty()
403    }
404}
405
406#[cfg(feature = "pyo3")]
407/// Python constructor for DMSCConfig
408#[pyo3::prelude::pymethods]
409impl DMSCConfig {
410    #[new]
411    fn py_new() -> Self {
412        Self::new()
413    }
414    
415    #[pyo3(name = "set")]
416    fn set_impl(&mut self, key: String, value: String) {
417        self.set(key, value);
418    }
419    
420    #[pyo3(name = "get")]
421    fn get_impl(&self, key: String) -> Option<String> {
422        self.get(&key).cloned()
423    }
424    
425    #[pyo3(name = "get_f64")]
426    fn get_f64_impl(&self, key: String) -> Option<f64> {
427        self.get_f64(&key)
428    }
429    
430    #[pyo3(name = "get_usize")]
431    fn get_usize_impl(&self, key: String) -> Option<usize> {
432        self.get_usize(&key)
433    }
434    
435    #[pyo3(name = "keys")]
436    fn py_keys(&self) -> Vec<String> {
437        self.keys().iter().map(|s| s.to_string()).collect()
438    }
439    
440    #[pyo3(name = "values")]
441    fn py_values(&self) -> Vec<String> {
442        self.all_values().iter().map(|s| s.to_string()).collect()
443    }
444    
445    #[pyo3(name = "contains")]
446    fn py_contains(&self, key: String) -> bool {
447        self.has_key(&key)
448    }
449    
450    #[pyo3(name = "len")]
451    fn py_len(&self) -> usize {
452        self.count()
453    }
454}
455
456/// Internal enum for different configuration source types.
457/// 
458/// This enum represents the different types of configuration sources that the
459/// `DMSCConfigManager` can handle.
460#[derive(Clone)]
461enum DMSCConfigSource {
462    /// File-based configuration source
463    File(PathBuf),
464    /// Environment variable configuration source
465    Environment,
466}
467
468/// Public-facing configuration manager with hot reload support.
469/// 
470/// This struct manages multiple configuration sources, loads configuration values,
471/// and provides access to the configuration. It supports hot reload and multiple
472/// configuration formats.
473#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
474pub struct DMSCConfigManager {
475    /// Internal configuration storage
476    config: Arc<RwLock<DMSCConfig>>,
477    /// List of configuration sources to load from
478    sources: Vec<DMSCConfigSource>,
479    /// Optional hook bus for emitting config reload events
480    #[cfg(feature = "pyo3")]
481    hooks: Option<Arc<crate::hooks::DMSCHookBus>>,
482    /// Hot reload watcher handle
483    #[cfg(feature = "config_hot_reload")]
484    watcher: Option<Arc<RecommendedWatcher>>,
485    /// Background task handle for the watcher
486    watcher_task: Arc<TokioRwLock<Option<JoinHandle<()>>>>,
487    /// Monitored file paths for hot reload
488    monitored_paths: Arc<TokioRwLock<Vec<PathBuf>>>,
489    /// Callback for config changes
490    #[cfg(feature = "config_hot_reload")]
491    change_callback: Option<Arc<dyn Fn() + Send + Sync>>,
492}
493
494impl Default for DMSCConfigManager {
495    fn default() -> Self {
496        Self::new()
497    }
498}
499
500impl Clone for DMSCConfigManager {
501    fn clone(&self) -> Self {
502        DMSCConfigManager {
503            config: self.config.clone(),
504            sources: self.sources.clone(),
505            #[cfg(feature = "pyo3")]
506            hooks: self.hooks.clone(),
507            #[cfg(feature = "config_hot_reload")]
508            watcher: self.watcher.clone(),
509            watcher_task: self.watcher_task.clone(),
510            monitored_paths: self.monitored_paths.clone(),
511            #[cfg(feature = "config_hot_reload")]
512            change_callback: self.change_callback.clone(),
513        }
514    }
515}
516
517impl DMSCConfigManager {
518    /// Creates a new empty configuration manager.
519    /// 
520    /// Returns a new `DMSCConfigManager` instance with no configuration sources.
521    pub fn new() -> Self {
522        DMSCConfigManager {
523            config: Arc::new(RwLock::new(DMSCConfig::new())),
524            sources: Vec::new(),
525            #[cfg(feature = "pyo3")]
526            hooks: None,
527            #[cfg(feature = "config_hot_reload")]
528            watcher: None,
529            watcher_task: Arc::new(TokioRwLock::new(None)),
530            monitored_paths: Arc::new(TokioRwLock::new(Vec::new())),
531            #[cfg(feature = "config_hot_reload")]
532            change_callback: None,
533        }
534    }
535
536    /// Creates a new configuration manager with the provided hook bus.
537    /// 
538    /// This method allows the config manager to emit hooks when configuration is reloaded.
539    /// 
540    /// # Parameters
541    /// 
542    /// - `hooks`: The hook bus to use for emitting events
543    /// 
544    /// # Returns
545    /// 
546    /// A new `DMSCConfigManager` instance with the provided hook bus
547    #[cfg(feature = "pyo3")]
548    pub fn with_hooks(hooks: Arc<crate::hooks::DMSCHookBus>) -> Self {
549        DMSCConfigManager {
550            config: Arc::new(RwLock::new(DMSCConfig::new())),
551            sources: Vec::new(),
552            hooks: Some(hooks),
553            #[cfg(feature = "config_hot_reload")]
554            watcher: None,
555            watcher_task: Arc::new(TokioRwLock::new(None)),
556            monitored_paths: Arc::new(TokioRwLock::new(Vec::new())),
557            #[cfg(feature = "config_hot_reload")]
558            change_callback: None,
559        }
560    }
561
562    /// Adds a file-based configuration source.
563    /// 
564    /// # Parameters
565    /// 
566    /// - `path`: The path to the configuration file
567    /// 
568    /// Supported file formats: JSON, YAML, TOML
569    pub fn add_file_source(&mut self, path: impl AsRef<Path>) {
570        self.sources.push(DMSCConfigSource::File(path.as_ref().to_path_buf()));
571    }
572
573    /// Adds environment variables as a configuration source.
574    /// 
575    /// Environment variables with the prefix `DMSC_` are loaded as configuration values.
576    /// Double underscores (`__`) in environment variable names are converted to dots.
577    /// For example, `DMSC_SERVER__PORT=8080` becomes `server.port=8080`.
578    pub fn add_environment_source(&mut self) {
579        self.sources.push(DMSCConfigSource::Environment);
580    }
581
582    /// Notifies registered hooks when configuration is reloaded.
583    #[cfg(feature = "pyo3")]
584    fn notify_config_reload(&self, _path: &str) {
585        if let Some(hooks) = &self.hooks {
586            let _ = hooks.emit_with(
587                &DMSCHookKind::ConfigReload,
588                &DMSCServiceContext::new_default().unwrap_or_else(|_| {
589                    DMSCServiceContext::new_with(
590                        crate::fs::DMSCFileSystem::new_auto_root().unwrap_or_else(|_| crate::fs::DMSCFileSystem::new_with_root(std::env::current_dir().unwrap_or_default())),
591                        crate::log::DMSCLogger::new(&crate::log::DMSCLogConfig::default(), crate::fs::DMSCFileSystem::new_with_root(std::env::current_dir().unwrap_or_default())),
592                        crate::config::DMSCConfigManager::new(),
593                        crate::hooks::DMSCHookBus::new(),
594                        None,
595                    )
596                }),
597                Some("config_manager"),
598                None,
599            );
600        }
601    }
602
603    /// Loads configuration from all registered sources.
604    /// 
605    /// This method loads configuration from all registered sources in the order they were added,
606    /// with later sources overriding earlier ones.
607    /// 
608    /// # Returns
609    /// 
610    /// A `Result<(), DMSCError>` indicating success or failure
611    pub fn load(&mut self) -> Result<(), crate::core::DMSCError> {
612        let mut cfg = DMSCConfig::new();
613
614        for source in &self.sources {
615            match source {
616                DMSCConfigSource::File(path) => {
617                    self.load_file(path, &mut cfg)?;
618                    #[cfg(feature = "pyo3")]
619                    self.notify_config_reload(path.to_str().unwrap_or(""));
620                }
621                DMSCConfigSource::Environment => {
622                    self.load_environment(&mut cfg);
623                }
624            }
625        }
626
627        *self.config.write().expect("Failed to lock config for writing") = cfg;
628        
629        Ok(())
630    }
631
632    /// Creates a new configuration manager with default sources.
633    /// 
634    /// This method creates a new `DMSCConfigManager` with the following default sources:
635    /// 1. Configuration files in the `config` directory (dms.yaml, dms.yml, dms.toml, dms.json)
636    /// 2. Environment variables with the prefix `DMSC_`
637    /// 
638    /// It also loads the configuration immediately.
639    /// 
640    /// # Returns
641    /// 
642    /// A new `DMSCConfigManager` instance with default sources and loaded configuration
643    pub fn new_default() -> Self {
644        let mut manager = Self::new();
645        
646        // Add default configuration sources
647        if let Ok(cwd) = std::env::current_dir() {
648            let config_dir = cwd.join("config");
649            
650            // Add all supported config files in order of priority (lowest to highest)
651            manager.add_file_source(config_dir.join("dms.yaml"));
652            manager.add_file_source(config_dir.join("dms.yml"));
653            manager.add_file_source(config_dir.join("dms.toml"));
654            manager.add_file_source(config_dir.join("dms.json"));
655        }
656        
657        // Add environment variables as highest priority
658        manager.add_environment_source();
659        
660        // Load configuration immediately
661        let _ = manager.load();
662        
663        manager
664    }
665
666    /// Loads configuration from a file.
667    /// 
668    /// This method loads configuration from a file, parses it based on its extension,
669    /// and flattens it into the provided configuration object.
670    /// 
671    /// # Parameters
672    /// 
673    /// - `path`: The path to the configuration file
674    /// - `cfg`: The configuration object to load values into
675    /// 
676    /// # Returns
677    /// 
678    /// A `Result<(), DMSCError>` indicating success or failure
679    fn load_file(&self, path: &Path, cfg: &mut DMSCConfig) -> Result<(), crate::core::DMSCError> {
680        if !path.exists() {
681            return Ok(());
682        }
683
684        let text = fs::read_to_string(path)?;
685        let extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
686
687        match extension.to_lowercase().as_str() {
688            "json" => {
689                if let Ok(map) = serde_json::from_str::<serde_json::Value>(&text) {
690                    self.flatten_json(&map, "", cfg);
691                }
692            }
693            "yaml" | "yml" => {
694                if let Ok(yaml_docs) = YamlLoader::load_from_str(&text) {
695                    for doc in yaml_docs {
696                        self.flatten_yaml(&doc, "", cfg);
697                    }
698                }
699            }
700            "toml" => {
701                if let Ok(toml) = toml::from_str(&text) {
702                    self.flatten_toml(&toml, "", cfg);
703                }
704            }
705            _ => {
706                // Ignore unsupported file types
707            }
708        }
709
710        Ok(())
711    }
712
713    /// Flattens a JSON value into the configuration.
714    /// 
715    /// This method recursively flattens a JSON value into the configuration using dot notation.
716    /// 
717    /// # Parameters
718    /// 
719    /// - `value`: The JSON value to flatten
720    /// - `prefix`: The current prefix for keys (used for recursion)
721    /// - `cfg`: The configuration object to load values into
722    fn flatten_json(&self, value: &serde_json::Value, prefix: &str, cfg: &mut DMSCConfig) {
723        Self::flatten_json_static(value, prefix, cfg);
724    }
725
726    /// Static version of `flatten_json` for recursion.
727    /// 
728    /// This static method is used for recursion to avoid the "parameter is only used in recursion" warning.
729    /// 
730    /// # Parameters
731    /// 
732    /// - `value`: The JSON value to flatten
733    /// - `prefix`: The current prefix for keys (used for recursion)
734    /// - `cfg`: The configuration object to load values into
735    fn flatten_json_static(value: &serde_json::Value, prefix: &str, cfg: &mut DMSCConfig) {
736        match value {
737            serde_json::Value::Object(map) => {
738                for (k, v) in map {
739                    let new_prefix = if prefix.is_empty() {
740                        k.clone()
741                    } else {
742                        format!("{prefix}.{k}")
743                    };
744                    Self::flatten_json_static(v, &new_prefix, cfg);
745                }
746            }
747            serde_json::Value::Array(arr) => {
748                for (i, v) in arr.iter().enumerate() {
749                    let new_prefix = format!("{prefix}.{i}");
750                    Self::flatten_json_static(v, &new_prefix, cfg);
751                }
752            }
753            serde_json::Value::String(s) => {
754                cfg.set(prefix, s);
755            }
756            serde_json::Value::Number(n) => {
757                cfg.set(prefix, n.to_string());
758            }
759            serde_json::Value::Bool(b) => {
760                cfg.set(prefix, b.to_string());
761            }
762            serde_json::Value::Null => {
763                cfg.set(prefix, "");
764            }
765        }
766    }
767
768    /// Flattens a YAML value into the configuration.
769    /// 
770    /// This method recursively flattens a YAML value into the configuration using dot notation.
771    /// 
772    /// # Parameters
773    /// 
774    /// - `value`: The YAML value to flatten
775    /// - `prefix`: The current prefix for keys (used for recursion)
776    /// - `cfg`: The configuration object to load values into
777    fn flatten_yaml(&self, value: &Yaml, prefix: &str, cfg: &mut DMSCConfig) {
778        Self::flatten_yaml_static(value, prefix, cfg);
779    }
780
781    /// Static version of `flatten_yaml` for recursion.
782    /// 
783    /// This static method is used for recursion to avoid the "parameter is only used in recursion" warning.
784    /// 
785    /// # Parameters
786    /// 
787    /// - `value`: The YAML value to flatten
788    /// - `prefix`: The current prefix for keys (used for recursion)
789    /// - `cfg`: The configuration object to load values into
790    fn flatten_yaml_static(value: &Yaml, prefix: &str, cfg: &mut DMSCConfig) {
791        match value {
792            Yaml::Hash(map) => {
793                for (k, v) in map {
794                    if let Yaml::String(key) = k {
795                        let new_prefix = if prefix.is_empty() {
796                            key.clone()
797                        } else {
798                            format!("{prefix}.{key}")
799                        };
800                        Self::flatten_yaml_static(v, &new_prefix, cfg);
801                    }
802                }
803            }
804            Yaml::Array(arr) => {
805                for (i, v) in arr.iter().enumerate() {
806                    let new_prefix = format!("{prefix}.{i}");
807                    Self::flatten_yaml_static(v, &new_prefix, cfg);
808                }
809            }
810            Yaml::String(s) => {
811                cfg.set(prefix, s);
812            }
813            Yaml::Integer(n) => {
814                cfg.set(prefix, n.to_string());
815            }
816            Yaml::Real(r) => {
817                cfg.set(prefix, r);
818            }
819            Yaml::Boolean(b) => {
820                cfg.set(prefix, b.to_string());
821            }
822            Yaml::Null => {
823                cfg.set(prefix, "");
824            }
825            _ => {
826                // Ignore other YAML types
827            }
828        }
829    }
830
831    /// Flattens a TOML value into the configuration.
832    /// 
833    /// This method recursively flattens a TOML value into the configuration using dot notation.
834    /// 
835    /// # Parameters
836    /// 
837    /// - `value`: The TOML value to flatten
838    /// - `prefix`: The current prefix for keys (used for recursion)
839    /// - `cfg`: The configuration object to load values into
840    fn flatten_toml(&self, value: &toml::Value, prefix: &str, cfg: &mut DMSCConfig) {
841        Self::flatten_toml_static(value, prefix, cfg);
842    }
843
844    /// Static version of `flatten_toml` for recursion.
845    /// 
846    /// This static method is used for recursion to avoid the "parameter is only used in recursion" warning.
847    /// 
848    /// # Parameters
849    /// 
850    /// - `value`: The TOML value to flatten
851    /// - `prefix`: The current prefix for keys (used for recursion)
852    /// - `cfg`: The configuration object to load values into
853    fn flatten_toml_static(value: &toml::Value, prefix: &str, cfg: &mut DMSCConfig) {
854        match value {
855            toml::Value::Table(table) => {
856                for (k, v) in table {
857                    let new_prefix = if prefix.is_empty() {
858                        k.clone()
859                    } else {
860                        format!("{prefix}.{k}")
861                    };
862                    Self::flatten_toml_static(v, &new_prefix, cfg);
863                }
864            }
865            toml::Value::Array(arr) => {
866                for (i, v) in arr.iter().enumerate() {
867                    let new_prefix = format!("{prefix}.{i}");
868                    Self::flatten_toml_static(v, &new_prefix, cfg);
869                }
870            }
871            toml::Value::String(s) => {
872                cfg.set(prefix, s);
873            }
874            toml::Value::Integer(n) => {
875                cfg.set(prefix, n.to_string());
876            }
877            toml::Value::Float(f) => {
878                cfg.set(prefix, f.to_string());
879            }
880            toml::Value::Boolean(b) => {
881                cfg.set(prefix, b.to_string());
882            }
883            toml::Value::Datetime(dt) => {
884                cfg.set(prefix, dt.to_string());
885            }
886        }
887    }
888
889    /// Loads configuration from environment variables.
890    /// 
891    /// This method loads environment variables with the prefix `DMSC_` into the configuration.
892    /// Double underscores (`__`) in environment variable names are converted to dots.
893    /// 
894    /// # Parameters
895    /// 
896    /// - `cfg`: The configuration object to load values into
897    fn load_environment(&self, cfg: &mut DMSCConfig) {
898        for (name, value) in std::env::vars() {
899            if let Some(rest) = name.strip_prefix("DMSC_") {
900                let key_parts: Vec<String> = rest
901                    .split("__")
902                    .map(|part| part.to_ascii_lowercase())
903                    .collect();
904                let key = key_parts.join(".");
905                if !key.is_empty() {
906                    cfg.set(key, value);
907                }
908            }
909        }
910    }
911
912    /// Starts the configuration watcher for hot reload.
913    /// 
914    /// This method starts watching all registered file-based configuration sources
915    /// for changes. When a configuration file is modified, it will be automatically
916    /// reloaded and the change callback (if registered) will be invoked.
917    /// 
918    /// # Returns
919    /// 
920    /// A `Result<(), DMSCError>` indicating success or failure
921    #[cfg(feature = "config_hot_reload")]
922    pub async fn start_watcher(&mut self) -> Result<(), crate::core::DMSCError> {
923        self.start_watcher_with_callback::<fn()>(None).await
924    }
925
926    /// Starts the configuration watcher with a custom change callback.
927    /// 
928    /// This method starts watching all registered file-based configuration sources
929    /// for changes. When a configuration file is modified, it will be automatically
930    /// reloaded and the provided callback will be invoked.
931    /// 
932    /// # Parameters
933    /// 
934    /// - `callback`: Optional callback function to invoke when configuration changes
935    /// 
936    /// # Returns
937    /// 
938    /// A `Result<(), DMSCError>` indicating success or failure
939    #[cfg(feature = "config_hot_reload")]
940    pub async fn start_watcher_with_callback<F>(&mut self, callback: Option<Arc<dyn Fn() + Send + Sync>>) -> Result<(), crate::core::DMSCError> {
941        let (tx, mut rx) = tokio::sync::mpsc::channel::<notify::Result<notify::Event>>(100);
942        
943        let mut watcher = RecommendedWatcher::new(
944            move |res| {
945                let _ = tx.blocking_send(res);
946            },
947            notify::Config::default(),
948        ).map_err(|e| crate::core::DMSCError::Config(format!("Failed to create config watcher: {}", e)))?;
949        
950        let mut monitored = Vec::new();
951        
952        for source in &self.sources {
953            if let DMSCConfigSource::File(path) = source {
954                if path.exists() {
955                    watcher.watch(path, RecursiveMode::NonRecursive)
956                        .map_err(|e| crate::core::DMSCError::Config(format!("Failed to watch config file {}: {}", path.display(), e)))?;
957                    monitored.push(path.clone());
958                }
959            }
960        }
961        
962        let monitored_paths = self.monitored_paths.clone();
963        let manager = self.clone();
964        let change_callback = callback.clone();
965        
966        let task = tokio::spawn(async move {
967            while let Some(event) = rx.recv().await {
968                match event {
969                    Ok(event) => {
970                        if let Some(paths) = event.paths.first() {
971                            let changed_path = paths.clone();
972                            
973                            {
974                                let mut paths_guard = monitored_paths.write().await;
975                                if !paths_guard.contains(&changed_path) {
976                                    paths_guard.push(changed_path.clone());
977                                }
978                            }
979                            
980                            log::info!("Config file changed: {}", changed_path.display());
981                            
982                            if let Err(e) = manager.reload_file(&changed_path).await {
983                                log::error!("Failed to reload config file {}: {}", changed_path.display(), e);
984                            }
985                            
986                            if let Some(ref cb) = change_callback {
987                                cb();
988                            }
989                            
990                            #[cfg(feature = "pyo3")]
991                            manager.notify_config_reload(changed_path.to_str().unwrap_or(""));
992                        }
993                    }
994                    Err(e) => {
995                        log::warn!("Config watcher error: {:?}", e);
996                    }
997                }
998            }
999        });
1000        
1001        self.watcher = Some(Arc::new(watcher));
1002        *self.watcher_task.write().await = Some(task);
1003        self.change_callback = callback;
1004        
1005        let mut paths_guard = self.monitored_paths.write().await;
1006        *paths_guard = monitored;
1007        
1008        Ok(())
1009    }
1010
1011    /// Reloads configuration from a specific file.
1012    /// 
1013    /// # Parameters
1014    /// 
1015    /// - `path`: The path to the configuration file to reload
1016    /// 
1017    /// # Returns
1018    /// 
1019    /// A `Result<(), DMSCError>` indicating success or failure
1020    #[cfg(feature = "config_hot_reload")]
1021    async fn reload_file(&self, path: &PathBuf) -> Result<(), crate::core::DMSCError> {
1022        let mut new_config = self.config.read().expect("Failed to lock config for reading").clone();
1023        self.load_file(path, &mut new_config)?;
1024        
1025        *self.config.write().expect("Failed to lock config for writing") = new_config;
1026        
1027        Ok(())
1028    }
1029
1030    /// Stops the configuration watcher.
1031    /// 
1032    /// This method stops the configuration watcher and cleans up associated resources.
1033    /// 
1034    /// # Returns
1035    /// 
1036    /// A `Result<(), DMSCError>` indicating success or failure
1037    #[cfg(feature = "config_hot_reload")]
1038    pub async fn stop_watcher(&mut self) -> Result<(), crate::core::DMSCError> {
1039        let task = self.watcher_task.write().await.take();
1040        if let Some(task) = task {
1041            task.abort();
1042        }
1043        
1044        self.watcher = None;
1045        
1046        let mut paths_guard = self.monitored_paths.write().await;
1047        paths_guard.clear();
1048        
1049        Ok(())
1050    }
1051
1052    /// Gets the list of monitored configuration file paths.
1053    /// 
1054    /// # Returns
1055    /// 
1056    /// A vector of paths being monitored for changes
1057    pub async fn get_monitored_paths(&self) -> Vec<PathBuf> {
1058        self.monitored_paths.read().await.clone()
1059    }
1060
1061    /// Starts the configuration watcher for hot reload.
1062    /// 
1063    /// This is a no-op implementation when the `config_hot_reload` feature is not enabled.
1064    /// 
1065    /// # Returns
1066    /// 
1067    /// A `Result<(), DMSCError>` indicating success or failure
1068    #[cfg(not(feature = "config_hot_reload"))]
1069    pub async fn start_watcher(&mut self) -> Result<(), crate::core::DMSCError> {
1070        Ok(())
1071    }
1072
1073    /// Gets a reference to the loaded configuration.
1074    /// 
1075    /// # Returns
1076    /// 
1077    /// A `DMSCConfig` clone of the loaded configuration
1078    pub fn config(&self) -> DMSCConfig {
1079        self.config.read().expect("Failed to lock config for reading").clone()
1080    }
1081
1082    /// Gets a mutable reference to the loaded configuration.
1083    /// 
1084    /// # Returns
1085    /// 
1086    /// A `std::sync::RwLockWriteGuard<DMSCConfig>` for the loaded configuration
1087    pub fn config_mut(&mut self) -> std::sync::RwLockWriteGuard<'_, DMSCConfig> {
1088        self.config.write().expect("Failed to lock config for writing")
1089    }
1090}
1091
1092#[cfg(feature = "pyo3")]
1093/// Python constructor for DMSCConfigManager
1094#[pyo3::prelude::pymethods]
1095impl DMSCConfigManager {
1096    #[new]
1097    fn py_new() -> Self {
1098        Self::new()
1099    }
1100    
1101    /// Adds a file-based configuration source from Python
1102    ///
1103    /// ## Supported Formats
1104    ///
1105    /// - `.json`: JSON configuration format
1106    /// - `.yaml`, `.yml`: YAML configuration format
1107    /// - `.toml`: TOML configuration format
1108    #[pyo3(name = "add_file_source")]
1109    fn add_file_source_impl(&mut self, path: String) {
1110        self.add_file_source(path);
1111    }
1112
1113    /// Adds environment variables as a configuration source from Python
1114    ///
1115    /// Environment variables are prefixed with `DMSC_` and double underscores
1116    /// are converted to dots (`.`) in the configuration key hierarchy.
1117    /// Example: `DMSC_DATABASE__HOST` becomes `database.host`
1118    #[pyo3(name = "add_environment_source")]
1119    fn add_environment_source_impl(&mut self) {
1120        self.add_environment_source();
1121    }
1122
1123    /// Gets a configuration value as string from Python
1124    ///
1125    /// This method retrieves a configuration value by key, returning it as a string.
1126    /// If the key does not exist, returns `None`.
1127    ///
1128    /// ## Parameters
1129    ///
1130    /// - `key`: Configuration key string (dot-notation supported, e.g., "server.port")
1131    ///
1132    /// ## Returns
1133    ///
1134    /// Optional string containing the configuration value if found, `None` otherwise
1135    ///
1136    /// ## Example
1137    ///
1138    /// ```python
1139    /// manager = DMSCConfigManager.new_default()
1140    /// value = manager.get("server.port")
1141    /// if value:
1142    ///     print(f"Server port: {value}")
1143    /// ```
1144    #[pyo3(name = "get")]
1145    fn get_config_impl(&self, key: String) -> Option<String> {
1146        self.config().get(&key).cloned()
1147    }
1148}
1149
1150#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
1151#[derive(Clone, Debug)]
1152pub struct DMSCConfigValidator {
1153    required_keys: Vec<String>,
1154    port_keys: Vec<String>,
1155    timeout_keys: Vec<String>,
1156    secret_keys: Vec<String>,
1157    url_keys: Vec<String>,
1158    positive_int_keys: Vec<String>,
1159}
1160
1161impl Default for DMSCConfigValidator {
1162    fn default() -> Self {
1163        Self::new()
1164    }
1165}
1166
1167impl DMSCConfigValidator {
1168    pub fn new() -> Self {
1169        DMSCConfigValidator {
1170            required_keys: Vec::new(),
1171            port_keys: vec!["server.port".to_string(), "cache.redis.port".to_string(), "database.port".to_string()],
1172            timeout_keys: vec!["server.timeout".to_string(), "cache.ttl".to_string(), "session.timeout".to_string()],
1173            secret_keys: vec!["auth.jwt.secret".to_string(), "auth.password.salt".to_string(), "encryption.key".to_string()],
1174            url_keys: vec!["database.url".to_string(), "cache.redis.url".to_string(), "mq.url".to_string()],
1175            positive_int_keys: vec!["pool.size".to_string(), "worker.count".to_string(), "retry.max".to_string()],
1176        }
1177    }
1178
1179    pub fn add_required(&mut self, key: String) -> &mut Self {
1180        self.required_keys.push(key);
1181        self
1182    }
1183
1184    pub fn add_port_check(&mut self, key: String) -> &mut Self {
1185        self.port_keys.push(key);
1186        self
1187    }
1188
1189    pub fn add_timeout_check(&mut self, key: String) -> &mut Self {
1190        self.timeout_keys.push(key);
1191        self
1192    }
1193
1194    pub fn add_secret_check(&mut self, key: String) -> &mut Self {
1195        self.secret_keys.push(key);
1196        self
1197    }
1198
1199    pub fn add_url_check(&mut self, key: String) -> &mut Self {
1200        self.url_keys.push(key);
1201        self
1202    }
1203
1204    pub fn add_positive_int_check(&mut self, key: String) -> &mut Self {
1205        self.positive_int_keys.push(key);
1206        self
1207    }
1208
1209    pub fn validate_config(&self, config: &DMSCConfig) -> Result<(), crate::core::DMSCError> {
1210        for key in &self.required_keys {
1211            if !config.has_key(key) {
1212                return Err(crate::core::DMSCError::Config(format!(
1213                    "Missing required configuration key: {}", key
1214                )));
1215            }
1216        }
1217
1218        for key in &self.port_keys {
1219            if let Some(port) = config.get_port(key) {
1220                if port == 0 {
1221                    return Err(crate::core::DMSCError::Config(format!(
1222                        "Invalid port number for {}: must be between 1 and 65535", key
1223                    )));
1224                }
1225            }
1226        }
1227
1228        for key in &self.timeout_keys {
1229            if let Some(timeout) = config.get_timeout_secs(key) {
1230                if timeout == 0 {
1231                    return Err(crate::core::DMSCError::Config(format!(
1232                        "Invalid timeout for {}: must be between 1 and 86400 seconds", key
1233                    )));
1234                }
1235            }
1236        }
1237
1238        for key in &self.secret_keys {
1239            if let Some(secret) = config.get_str(key) {
1240                if secret.len() < 8 {
1241                    return Err(crate::core::DMSCError::Config(format!(
1242                        "Secret key {} is too short: minimum length is 8 characters", key
1243                    )));
1244                }
1245                if secret == "secret" || secret == "password" || secret == "123456" {
1246                    return Err(crate::core::DMSCError::Config(format!(
1247                        "Insecure secret key detected for {}: using default or weak value", key
1248                    )));
1249                }
1250            }
1251        }
1252
1253        for key in &self.url_keys {
1254            if let Some(url) = config.get_str(key) {
1255                if !url.starts_with("http://") && !url.starts_with("https://")
1256                    && !url.starts_with("redis://") && !url.starts_with("postgresql://")
1257                    && !url.starts_with("mysql://") && !url.starts_with("amqp://")
1258                    && !url.starts_with("kafka://") && !url.starts_with("sqlite://")
1259                {
1260                    return Err(crate::core::DMSCError::Config(format!(
1261                        "Invalid URL format for {}: {}", key, url
1262                    )));
1263                }
1264            }
1265        }
1266
1267        for key in &self.positive_int_keys {
1268            if let Some(value) = config.get_u32(key) {
1269                if value == 0 {
1270                    return Err(crate::core::DMSCError::Config(format!(
1271                        "Invalid value for {}: must be a positive integer", key
1272                    )));
1273                }
1274            }
1275        }
1276
1277        Ok(())
1278    }
1279
1280    pub fn validate_with_requirements(
1281        &self,
1282        config: &DMSCConfig,
1283        requirements: &[String],
1284    ) -> Result<(), crate::core::DMSCError> {
1285        for key in requirements {
1286            if !config.has_key(key) {
1287                return Err(crate::core::DMSCError::Config(format!(
1288                    "Missing required configuration key: {}", key
1289                )));
1290            }
1291        }
1292        self.validate_config(config)
1293    }
1294}
1295
1296#[cfg(feature = "pyo3")]
1297#[pyo3::prelude::pymethods]
1298impl DMSCConfigValidator {
1299    #[new]
1300    fn py_new() -> Self {
1301        Self::new()
1302    }
1303
1304    #[pyo3(name = "require")]
1305    fn py_add_required(&mut self, key: String) {
1306        self.required_keys.push(key);
1307    }
1308
1309    #[pyo3(name = "require_port")]
1310    fn py_add_port_check(&mut self, key: String) {
1311        self.port_keys.push(key);
1312    }
1313
1314    #[pyo3(name = "require_timeout")]
1315    fn py_add_timeout_check(&mut self, key: String) {
1316        self.timeout_keys.push(key);
1317    }
1318
1319    #[pyo3(name = "require_secret")]
1320    fn py_add_secret_check(&mut self, key: String) {
1321        self.secret_keys.push(key);
1322    }
1323
1324    #[pyo3(name = "require_url")]
1325    fn py_add_url_check(&mut self, key: String) {
1326        self.url_keys.push(key);
1327    }
1328
1329    #[pyo3(name = "require_positive_int")]
1330    fn py_add_positive_int_check(&mut self, key: String) {
1331        self.positive_int_keys.push(key);
1332    }
1333
1334    #[pyo3(name = "validate")]
1335    fn py_validate(&self, config: &DMSCConfig) -> bool {
1336        self.validate_config(config).is_ok()
1337    }
1338}