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