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