Skip to main content

ri/config/
mod.rs

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