1use crate::{Config, ConfigError, ConfigResult, FileFormat, ReloadStrategy};
12use std::collections::HashMap;
13use std::path::PathBuf;
14use std::sync::Arc;
15use std::time::Duration;
16
17#[derive(Debug, Clone)]
26pub struct ConfigLoader {
27 config: Config,
30
31 search_paths: Vec<PathBuf>,
34
35 file_names: Vec<String>,
38
39 profiles: Vec<String>,
42
43 load_env: bool,
46
47 load_args: bool,
50}
51
52impl ConfigLoader {
53 pub fn new() -> Self {
56 Self {
57 config: Config::new(),
58 search_paths: vec![
59 PathBuf::from("./config"),
60 PathBuf::from("."),
61 PathBuf::from("/etc/hiver"),
62 ],
63 file_names: vec!["application".to_string()],
64 profiles: vec!["default".to_string()],
65 load_env: true,
66 load_args: true,
67 }
68 }
69
70 pub fn builder() -> ConfigLoaderBuilder {
73 ConfigLoaderBuilder::new()
74 }
75
76 pub fn add_search_path(mut self, path: impl Into<PathBuf>) -> Self {
79 self.search_paths.push(path.into());
80 self
81 }
82
83 pub fn add_file_name(mut self, name: impl Into<String>) -> Self {
86 self.file_names.push(name.into());
87 self
88 }
89
90 pub fn add_profile(mut self, profile: impl Into<String>) -> Self {
93 self.profiles.push(profile.into());
94 self
95 }
96
97 pub fn load_env(mut self, load: bool) -> Self {
100 self.load_env = load;
101 self
102 }
103
104 pub fn load_args(mut self, load: bool) -> Self {
107 self.load_args = load;
108 self
109 }
110
111 pub fn load(mut self) -> ConfigResult<Config> {
114 self.load_application_files()?;
117
118 self.load_profile_files()?;
120
121 if self.load_env {
123 self.load_environment_vars()?;
124 }
125
126 if self.load_args {
128 self.load_command_line_args()?;
129 }
130
131 Ok(self.config)
132 }
133
134 fn load_application_files(&mut self) -> ConfigResult<()> {
137 let formats = [
138 FileFormat::Properties,
139 FileFormat::Yaml,
140 FileFormat::Toml,
141 FileFormat::Json,
142 ];
143
144 for search_path in &self.search_paths {
145 for file_name in &self.file_names {
146 for format in &formats {
147 for ext in format.extensions() {
148 let path = search_path.join(format!("{}.{}", file_name, ext));
149 if path.exists() {
150 if let Err(e) = self.config.load_file(&path) {
151 tracing::debug!("Skipping {:?}: {}", path, e);
152 } else {
153 tracing::debug!("Loaded config from {:?}", path);
154 }
155 }
156 }
157 }
158 }
159 }
160
161 Ok(())
162 }
163
164 fn load_profile_files(&mut self) -> ConfigResult<()> {
167 let formats = [
168 FileFormat::Properties,
169 FileFormat::Yaml,
170 FileFormat::Toml,
171 FileFormat::Json,
172 ];
173
174 for profile in &self.profiles {
175 for search_path in &self.search_paths {
176 for file_name in &self.file_names {
177 for format in &formats {
178 for ext in format.extensions() {
179 let path =
180 search_path.join(format!("{}-{}.{}", file_name, profile, ext));
181 if path.exists() {
182 if let Err(e) = self.config.load_file(&path) {
183 tracing::debug!("Skipping {:?}: {}", path, e);
184 } else {
185 tracing::debug!(
186 "Loaded config from {:?} (profile: {})",
187 path,
188 profile
189 );
190 }
191 }
192 }
193 }
194 }
195 }
196 }
197
198 Ok(())
199 }
200
201 fn load_environment_vars(&mut self) -> ConfigResult<()> {
204 use crate::{PropertySourceBuilder, PropertySourceType, Value};
205
206 let mut builder = PropertySourceBuilder::new("environmentVariables")
207 .source_type(PropertySourceType::SystemEnvironment)
208 .order(200);
209
210 for (key, value) in std::env::vars() {
211 let config_key = key.to_lowercase().replace('_', ".");
213 builder.put(config_key, Value::string(value.clone()));
214 builder.put(key, Value::string(value));
215 }
216
217 self.config.add_property_source(builder.build());
218 Ok(())
219 }
220
221 fn load_command_line_args(&mut self) -> ConfigResult<()> {
224 use crate::{PropertySourceBuilder, PropertySourceType, Value};
225
226 let mut builder = PropertySourceBuilder::new("commandLineArgs")
227 .source_type(PropertySourceType::CommandLine)
228 .order(100);
229
230 let args: Vec<String> = std::env::args().collect();
231
232 for arg in args.iter().skip(1) {
233 if let Some(arg) = arg.strip_prefix("--") {
234 if let Some((key, value)) = arg.split_once('=') {
235 builder.put(key, Value::string(value));
236 } else {
237 builder.put(arg, Value::bool(true));
239 }
240 }
241 }
242
243 self.config.add_property_source(builder.build());
244 Ok(())
245 }
246}
247
248impl Default for ConfigLoader {
249 fn default() -> Self {
250 Self::new()
251 }
252}
253
254pub struct ConfigLoaderBuilder {
260 loader: ConfigLoader,
261}
262
263impl ConfigLoaderBuilder {
264 pub fn new() -> Self {
267 Self {
268 loader: ConfigLoader::new(),
269 }
270 }
271
272 pub fn search_path(mut self, path: impl Into<PathBuf>) -> Self {
275 self.loader = self.loader.add_search_path(path);
276 self
277 }
278
279 pub fn search_paths(mut self, paths: Vec<PathBuf>) -> Self {
282 for path in paths {
283 self.loader = self.loader.add_search_path(path);
284 }
285 self
286 }
287
288 pub fn file_name(mut self, name: impl Into<String>) -> Self {
291 self.loader = self.loader.add_file_name(name);
292 self
293 }
294
295 pub fn file_names(mut self, names: Vec<String>) -> Self {
298 for name in names {
299 self.loader = self.loader.add_file_name(name);
300 }
301 self
302 }
303
304 pub fn profile(mut self, profile: impl Into<String>) -> Self {
307 self.loader = self.loader.add_profile(profile);
308 self
309 }
310
311 pub fn profiles(mut self, profiles: Vec<String>) -> Self {
314 for profile in profiles {
315 self.loader = self.loader.add_profile(profile);
316 }
317 self
318 }
319
320 pub fn load_env(mut self, load: bool) -> Self {
323 self.loader = self.loader.load_env(load);
324 self
325 }
326
327 pub fn load_args(mut self, load: bool) -> Self {
330 self.loader = self.loader.load_args(load);
331 self
332 }
333
334 pub fn build(self) -> ConfigLoader {
337 self.loader
338 }
339
340 pub fn load(self) -> ConfigResult<Config> {
343 self.loader.load()
344 }
345}
346
347impl Default for ConfigLoaderBuilder {
348 fn default() -> Self {
349 Self::new()
350 }
351}
352
353pub struct Watcher {
359 config: Arc<Config>,
362
363 watched_files: Arc<std::sync::RwLock<HashMap<PathBuf, std::time::SystemTime>>>,
366
367 strategy: ReloadStrategy,
370
371 interval: Duration,
374
375 running: Arc<std::sync::atomic::AtomicBool>,
378}
379
380impl Watcher {
381 pub fn new(config: Arc<Config>) -> Self {
384 let strategy = config.reload_strategy();
385
386 Self {
387 config,
388 watched_files: Arc::new(std::sync::RwLock::new(HashMap::new())),
389 strategy,
390 interval: Duration::from_secs(5),
391 running: Arc::new(false.into()),
392 }
393 }
394
395 pub fn interval(mut self, interval: Duration) -> Self {
398 self.interval = interval;
399 self
400 }
401
402 pub fn watch_file(&self, path: PathBuf) {
405 if let Ok(metadata) = std::fs::metadata(&path)
406 && let Ok(modified) = metadata.modified()
407 {
408 let mut files = self
409 .watched_files
410 .write()
411 .unwrap_or_else(std::sync::PoisonError::into_inner);
412 files.insert(path, modified);
413 }
414 }
415
416 pub fn start(&self) -> ConfigResult<()> {
419 if self.strategy != ReloadStrategy::Watch {
420 return Err(ConfigError::OverrideNotAllowed(
421 "Watcher requires ReloadStrategy::Watch".to_string(),
422 ));
423 }
424
425 self.running
426 .store(true, std::sync::atomic::Ordering::SeqCst);
427
428 let config = self.config.clone();
429 let watched_files = self.watched_files.clone();
430 let running = self.running.clone();
431 let interval = self.interval;
432
433 std::thread::spawn(move || {
434 while running.load(std::sync::atomic::Ordering::SeqCst) {
435 std::thread::sleep(interval);
436
437 let mut files = watched_files
438 .write()
439 .unwrap_or_else(std::sync::PoisonError::into_inner);
440 let mut changed = Vec::new();
441
442 for (path, last_modified) in files.iter() {
443 if let Ok(metadata) = std::fs::metadata(path)
444 && let Ok(modified) = metadata.modified()
445 && modified != *last_modified
446 {
447 changed.push((path.clone(), modified));
448 }
449 }
450
451 for (path, modified) in changed {
452 tracing::info!("Config file changed: {:?}, reloading...", path);
453
454 if let Err(e) = config.load_file(&path) {
456 tracing::error!("Failed to reload config {:?}: {}", path, e);
457 } else {
458 tracing::info!("Successfully reloaded config from {:?}", path);
459 }
460
461 files.insert(path, modified);
462 }
463 }
464 });
465
466 Ok(())
467 }
468
469 pub fn stop(&self) {
472 self.running
473 .store(false, std::sync::atomic::Ordering::SeqCst);
474 }
475}
476
477pub(crate) trait ConfigPostProcessor: Send + Sync {
486 fn post_process(&self, config: &mut Config) -> ConfigResult<()>;
489}
490
491pub(crate) struct StandardPostProcessors;
494
495impl StandardPostProcessors {
496 pub(crate) fn placeholder_expander() -> impl ConfigPostProcessor {
499 PlaceholderExpander
500 }
501
502 pub(crate) fn required_validator(required: Vec<String>) -> impl ConfigPostProcessor {
505 RequiredValidator { required }
506 }
507}
508
509struct PlaceholderExpander;
512
513impl ConfigPostProcessor for PlaceholderExpander {
514 fn post_process(&self, _config: &mut Config) -> ConfigResult<()> {
515 Ok(())
518 }
519}
520
521struct RequiredValidator {
524 required: Vec<String>,
525}
526
527impl ConfigPostProcessor for RequiredValidator {
528 fn post_process(&self, config: &mut Config) -> ConfigResult<()> {
529 for key in &self.required {
530 if !config.contains_key(key) {
531 return Err(ConfigError::MissingProperty(key.clone()));
532 }
533 }
534 Ok(())
535 }
536}
537
538#[cfg(test)]
539mod tests {
540 use super::*;
541 use crate::{PropertySource, Value};
542
543 #[test]
544 fn test_loader_builder() {
545 let loader = ConfigLoaderBuilder::new()
546 .search_path("./test")
547 .profile("test")
548 .load_env(true)
549 .build();
550
551 assert_eq!(loader.profiles.len(), 2); }
553
554 #[test]
557 fn test_loader_new_defaults() {
558 let loader = ConfigLoader::new();
559 assert_eq!(loader.search_paths.len(), 3);
560 assert_eq!(loader.file_names.len(), 1);
561 assert_eq!(loader.file_names[0], "application");
562 assert_eq!(loader.profiles.len(), 1);
563 assert_eq!(loader.profiles[0], "default");
564 assert!(loader.load_env);
565 assert!(loader.load_args);
566 }
567
568 #[test]
571 fn test_loader_default() {
572 let loader = ConfigLoader::default();
573 assert_eq!(loader.search_paths.len(), 3);
574 }
575
576 #[test]
579 fn test_loader_builder_search_paths() {
580 let loader = ConfigLoaderBuilder::new()
581 .search_paths(vec![PathBuf::from("/a"), PathBuf::from("/b")])
582 .build();
583 assert!(loader.search_paths.len() >= 5);
585 }
586
587 #[test]
590 fn test_loader_builder_file_names() {
591 let loader = ConfigLoaderBuilder::new()
592 .file_names(vec!["custom".to_string(), "override".to_string()])
593 .build();
594 assert!(loader.file_names.len() >= 3);
596 }
597
598 #[test]
601 fn test_loader_builder_profiles() {
602 let loader = ConfigLoaderBuilder::new()
603 .profiles(vec!["staging".to_string(), "prod".to_string()])
604 .build();
605 assert!(loader.profiles.len() >= 3);
607 }
608
609 #[test]
612 fn test_loader_builder_disable_env_and_args() {
613 let loader = ConfigLoaderBuilder::new()
614 .load_env(false)
615 .load_args(false)
616 .build();
617 assert!(!loader.load_env);
618 assert!(!loader.load_args);
619 }
620
621 #[test]
624 fn test_loader_add_methods() {
625 let loader = ConfigLoader::new()
626 .add_search_path("/custom/path")
627 .add_file_name("myapp")
628 .add_profile("dev");
629
630 assert!(loader.search_paths.len() > 3);
631 assert!(loader.file_names.contains(&"myapp".to_string()));
632 assert!(loader.profiles.contains(&"dev".to_string()));
633 }
634
635 #[test]
638 fn test_loader_builder_default() {
639 let loader = ConfigLoaderBuilder::default().build();
640 assert_eq!(loader.search_paths.len(), 3);
641 }
642
643 #[test]
646 fn test_required_validator_pass() {
647 let mut config = Config::new();
648 let mut source = PropertySource::new("test");
649 source.put("db.url", Value::string("postgres://localhost"));
650 source.put("db.user", Value::string("admin"));
651 config.add_property_source(source);
652
653 let validator = StandardPostProcessors::required_validator(vec![
654 "db.url".to_string(),
655 "db.user".to_string(),
656 ]);
657 assert!(validator.post_process(&mut config).is_ok());
658 }
659
660 #[test]
663 fn test_required_validator_fail() {
664 let mut config = Config::new();
665 let source = PropertySource::new("test");
666 config.add_property_source(source);
667
668 let validator = StandardPostProcessors::required_validator(vec!["missing.key".to_string()]);
669 assert!(validator.post_process(&mut config).is_err());
670 }
671
672 #[test]
675 fn test_placeholder_expander() {
676 let mut config = Config::new();
677 let expander = StandardPostProcessors::placeholder_expander();
678 assert!(expander.post_process(&mut config).is_ok());
679 }
680
681 #[test]
684 fn test_loader_load_no_files() {
685 let result = ConfigLoaderBuilder::new()
686 .search_path("/nonexistent_hiver_path")
687 .load_env(false)
688 .load_args(false)
689 .load();
690 assert!(result.is_ok());
691 }
692}