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}