1use crate::{ConfigResult, PropertySource, Value, environment::Environment, error::ConfigError};
12use indexmap::IndexMap;
13use std::collections::HashMap;
14use std::path::{Path, PathBuf};
15use std::sync::{Arc, RwLock};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum FileFormat {
24 Properties,
27
28 Yaml,
31
32 Toml,
35
36 Json,
39}
40
41impl FileFormat {
42 pub fn extensions(&self) -> &[&str] {
45 match self {
46 FileFormat::Properties => &["properties", "props"],
47 FileFormat::Yaml => &["yaml", "yml"],
48 FileFormat::Toml => &["toml"],
49 FileFormat::Json => &["json"],
50 }
51 }
52
53 pub fn from_path(path: &Path) -> Option<Self> {
56 let ext = path.extension()?.to_str()?.to_lowercase();
57 match ext.as_str() {
58 "properties" | "props" => Some(FileFormat::Properties),
59 "yaml" | "yml" => Some(FileFormat::Yaml),
60 "toml" => Some(FileFormat::Toml),
61 "json" => Some(FileFormat::Json),
62 _ => None,
63 }
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum ReloadStrategy {
74 Never,
77
78 OnRequest,
81
82 Periodic(u64),
85
86 Watch,
89}
90
91#[derive(Debug, Clone)]
108pub struct Config {
109 environment: Arc<Environment>,
112
113 files: Arc<RwLock<Vec<PathBuf>>>,
116
117 reload_strategy: ReloadStrategy,
120
121 values: Arc<RwLock<IndexMap<String, Value>>>,
124}
125
126impl Config {
127 pub fn new() -> Self {
130 Self {
131 environment: Arc::new(Environment::new()),
132 files: Arc::new(RwLock::new(Vec::new())),
133 reload_strategy: ReloadStrategy::Never,
134 values: Arc::new(RwLock::new(IndexMap::new())),
135 }
136 }
137
138 pub fn builder() -> ConfigBuilder {
141 ConfigBuilder::new()
142 }
143
144 pub fn load() -> ConfigResult<Self> {
153 Self::builder().build()
154 }
155
156 pub fn from_file<P: AsRef<Path>>(path: P) -> ConfigResult<Self> {
159 Self::builder().add_file(path).build()
160 }
161
162 pub fn add_property_source(&self, source: PropertySource) {
165 self.environment.add_property_source(source);
166 self.invalidate_cache();
167 }
168
169 pub fn add_property_source_first(&self, source: PropertySource) {
172 self.environment.add_property_source_first(source);
173 self.invalidate_cache();
174 }
175
176 pub fn get(&self, key: &str) -> Option<Value> {
179 if let Ok(cache) = self.values.read()
181 && let Some(value) = cache.get(key)
182 {
183 return Some(value.clone());
184 }
185
186 let value = self.environment.get_property(key);
188
189 if let Some(ref v) = value
191 && let Ok(mut cache) = self.values.write()
192 {
193 cache.insert(key.to_string(), v.clone());
194 }
195
196 value
197 }
198
199 pub fn get_as<T>(&self, key: &str) -> ConfigResult<T>
202 where
203 T: serde::de::DeserializeOwned,
204 {
205 let value = self
206 .get(key)
207 .ok_or_else(|| ConfigError::MissingProperty(key.to_string()))?;
208
209 value.into()
210 }
211
212 pub fn get_required(&self, key: &str) -> ConfigResult<Value> {
215 self.get(key)
216 .ok_or_else(|| ConfigError::MissingProperty(key.to_string()))
217 }
218
219 pub fn get_required_as<T>(&self, key: &str) -> ConfigResult<T>
222 where
223 T: serde::de::DeserializeOwned,
224 {
225 let value = self.get_required(key)?;
226 value.into()
227 }
228
229 pub fn get_or<T>(&self, key: &str, default: T) -> T
232 where
233 T: serde::de::DeserializeOwned,
234 {
235 self.get_as(key).unwrap_or(default)
236 }
237
238 pub fn contains_key(&self, key: &str) -> bool {
241 self.get(key).is_some()
242 }
243
244 pub fn get_prefix(&self, prefix: &str) -> IndexMap<String, Value> {
247 let mut result = IndexMap::new();
248
249 let sources = self.environment.get_property_sources();
250 for source in sources {
251 for (key, value) in source.iter() {
252 if key.starts_with(prefix) {
253 result.entry(key.clone()).or_insert(value.clone());
254 }
255 }
256 }
257
258 result
259 }
260
261 pub fn environment(&self) -> &Environment {
264 &self.environment
265 }
266
267 pub fn files(&self) -> Vec<PathBuf> {
270 self.files
271 .read()
272 .unwrap_or_else(std::sync::PoisonError::into_inner)
273 .clone()
274 }
275
276 pub fn reload_strategy(&self) -> ReloadStrategy {
279 self.reload_strategy
280 }
281
282 pub fn reload(&self) -> ConfigResult<()> {
285 self.invalidate_cache();
287
288 if self.reload_strategy != ReloadStrategy::Never {
290 for file in self.files() {
291 self.load_file(&file)?;
292 }
293 }
294
295 Ok(())
296 }
297
298 fn invalidate_cache(&self) {
301 if let Ok(mut cache) = self.values.write() {
302 cache.clear();
303 }
304 }
305
306 pub(crate) fn load_file<P: AsRef<Path>>(&self, path: P) -> ConfigResult<()> {
309 let path = path.as_ref();
310 let format = FileFormat::from_path(path)
311 .ok_or_else(|| ConfigError::InvalidFormat(format!("{:?}", path)))?;
312
313 let content = std::fs::read_to_string(path)?;
314
315 let source = match format {
316 FileFormat::Properties => self.parse_properties(&content),
317 FileFormat::Yaml => self.parse_yaml(&content),
318 FileFormat::Toml => self.parse_toml(&content),
319 FileFormat::Json => self.parse_json(&content),
320 }?;
321
322 let mut source = source;
323 source.set_file_path(path.to_path_buf());
324
325 self.environment.add_property_source(source);
326
327 if let Ok(mut files) = self.files.write() {
328 let path_buf = path.to_path_buf();
329 if !files.contains(&path_buf) {
330 files.push(path_buf);
331 }
332 }
333
334 Ok(())
335 }
336
337 fn parse_properties(&self, content: &str) -> ConfigResult<PropertySource> {
340 let mut map = HashMap::new();
341
342 for line in content.lines() {
343 let line = line.trim();
344 if line.is_empty() || line.starts_with('#') || line.starts_with('!') {
345 continue;
346 }
347
348 if let Some((key, value)) = line.split_once('=') {
349 let key = key.trim().to_string();
350 let value = Self::unescape_value(value.trim());
351 map.insert(key, Value::string(value));
352 }
353 }
354
355 Ok(PropertySource::with_map("application.properties", map))
356 }
357
358 fn unescape_value(value: &str) -> String {
361 let mut result = String::new();
362 let mut chars = value.chars().peekable();
363
364 while let Some(c) = chars.next() {
365 if c == '\\' {
366 match chars.next() {
367 Some('n') => result.push('\n'),
368 Some('r') => result.push('\r'),
369 Some('t') => result.push('\t'),
370 Some('u') => {
371 let code: String = chars.by_ref().take(4).collect();
373 if let Ok(code_point) = u32::from_str_radix(&code, 16)
374 && let Some(c) = char::from_u32(code_point)
375 {
376 result.push(c);
377 }
378 },
379 Some(next) => result.push(next),
380 None => result.push('\\'),
381 }
382 } else {
383 result.push(c);
384 }
385 }
386
387 result
388 }
389
390 fn parse_yaml(&self, content: &str) -> ConfigResult<PropertySource> {
393 let yaml: serde_yaml::Value =
394 serde_yaml::from_str(content).map_err(|e| ConfigError::Parse(e.to_string()))?;
395
396 let map = Self::yaml_to_map(&yaml)?;
397 Ok(PropertySource::with_map("application.yaml", map))
398 }
399
400 fn yaml_to_map(yaml: &serde_yaml::Value) -> ConfigResult<HashMap<String, Value>> {
403 let mut map = HashMap::new();
404
405 if let serde_yaml::Value::Mapping(mapping) = yaml {
406 for (key, value) in mapping {
407 if let serde_yaml::Value::String(key_str) = key {
408 let value = Self::yaml_to_value(value)?;
409 map.insert(key_str.clone(), value);
410 }
411 }
412 }
413
414 Ok(map)
415 }
416
417 fn yaml_to_value(yaml: &serde_yaml::Value) -> ConfigResult<Value> {
420 Ok(match yaml {
421 serde_yaml::Value::Null | serde_yaml::Value::Tagged(_) => Value::Null,
422 serde_yaml::Value::Bool(v) => Value::Bool(*v),
423 serde_yaml::Value::Number(v) => {
424 if let Some(i) = v.as_i64() {
425 Value::Integer(i)
426 } else if let Some(f) = v.as_f64() {
427 Value::Float(f)
428 } else {
429 Value::Null
430 }
431 },
432 serde_yaml::Value::String(v) => Value::String(v.clone()),
433 serde_yaml::Value::Sequence(v) => Value::List(
434 v.iter()
435 .map(|x| Self::yaml_to_value(x))
436 .collect::<ConfigResult<Vec<_>>>()?,
437 ),
438 serde_yaml::Value::Mapping(v) => Value::Object(
439 v.iter()
440 .filter_map(|(k, v)| {
441 k.as_str()
442 .map(|key| (key.to_string(), Self::yaml_to_value(v).ok()))
443 })
444 .filter_map(|(k, v)| v.map(|val| (k, val)))
445 .collect(),
446 ),
447 })
448 }
449
450 fn parse_toml(&self, content: &str) -> ConfigResult<PropertySource> {
453 let toml: toml::Value =
454 toml::from_str(content).map_err(|e| ConfigError::Parse(e.to_string()))?;
455
456 let map = Self::toml_to_map(&toml)?;
457 Ok(PropertySource::with_map("application.toml", map))
458 }
459
460 fn toml_to_map(toml: &toml::Value) -> ConfigResult<HashMap<String, Value>> {
463 let mut map = HashMap::new();
464
465 if let toml::Value::Table(table) = toml {
466 for (key, value) in table {
467 map.insert(key.clone(), Self::toml_to_value(value));
468 }
469 }
470
471 Ok(map)
472 }
473
474 fn toml_to_value(toml: &toml::Value) -> Value {
477 match toml {
478 toml::Value::Boolean(v) => Value::Bool(*v),
479 toml::Value::Integer(v) => Value::Integer(*v),
480 toml::Value::Float(v) => Value::Float(*v),
481 toml::Value::String(v) => Value::String(v.clone()),
482 toml::Value::Array(v) => Value::List(v.iter().map(Self::toml_to_value).collect()),
483 toml::Value::Table(table) => Value::Object(
484 table
485 .iter()
486 .map(|(k, v)| (k.clone(), Self::toml_to_value(v)))
487 .collect(),
488 ),
489 toml::Value::Datetime(v) => Value::String(v.to_string()),
490 }
491 }
492
493 fn parse_json(&self, content: &str) -> ConfigResult<PropertySource> {
496 let json: serde_json::Value =
497 serde_json::from_str(content).map_err(|e| ConfigError::Parse(e.to_string()))?;
498
499 let map = Self::json_to_map(&json)?;
500 Ok(PropertySource::with_map("application.json", map))
501 }
502
503 fn json_to_map(json: &serde_json::Value) -> ConfigResult<HashMap<String, Value>> {
506 let mut map = HashMap::new();
507
508 if let serde_json::Value::Object(obj) = json {
509 for (key, value) in obj {
510 map.insert(key.clone(), Self::json_to_value(value));
511 }
512 }
513
514 Ok(map)
515 }
516
517 fn json_to_value(json: &serde_json::Value) -> Value {
520 match json {
521 serde_json::Value::Null => Value::Null,
522 serde_json::Value::Bool(v) => Value::Bool(*v),
523 serde_json::Value::Number(v) => {
524 if let Some(i) = v.as_i64() {
525 Value::Integer(i)
526 } else if let Some(f) = v.as_f64() {
527 Value::Float(f)
528 } else {
529 Value::Null
530 }
531 },
532 serde_json::Value::String(v) => Value::String(v.clone()),
533 serde_json::Value::Array(v) => Value::List(v.iter().map(Self::json_to_value).collect()),
534 serde_json::Value::Object(obj) => Value::Object(
535 obj.iter()
536 .map(|(k, v)| (k.clone(), Self::json_to_value(v)))
537 .collect(),
538 ),
539 }
540 }
541}
542
543impl Default for Config {
544 fn default() -> Self {
545 Self::new()
546 }
547}
548
549pub struct ConfigBuilder {
555 config: Config,
556}
557
558impl ConfigBuilder {
559 pub fn new() -> Self {
562 Self {
563 config: Config::new(),
564 }
565 }
566
567 pub fn add_file<P: AsRef<Path>>(self, path: P) -> Self {
570 let path = path.as_ref();
571 if let Err(e) = self.config.load_file(path) {
572 tracing::warn!("Failed to load config file {:?}: {}", path, e);
573 }
574 self
575 }
576
577 pub fn add_dir<P: AsRef<Path>>(mut self, dir: P) -> Self {
580 let dir = dir.as_ref();
581 if let Ok(entries) = std::fs::read_dir(dir) {
582 for entry in entries.flatten() {
583 let path = entry.path();
584 if path.is_file() && FileFormat::from_path(&path).is_some() {
585 self = self.add_file(path);
586 }
587 }
588 }
589 self
590 }
591
592 pub fn add_profile(self, profile: impl Into<crate::Profile>) -> Self {
595 self.config.environment.add_active_profile(profile.into());
596 self
597 }
598
599 pub fn set_profiles(self, profiles: Vec<crate::Profile>) -> Self {
602 self.config.environment.set_active_profiles(profiles);
603 self
604 }
605
606 pub fn add_property_source(self, source: PropertySource) -> Self {
609 self.config.add_property_source(source);
610 self
611 }
612
613 pub fn add_property(self, key: impl Into<String>, value: impl Into<Value>) -> Self {
616 let mut source = PropertySource::new("manual");
617 source.put(key, value);
618 self.config.add_property_source(source);
619 self
620 }
621
622 pub fn reload_strategy(mut self, strategy: ReloadStrategy) -> Self {
625 self.config.reload_strategy = strategy;
626 self
627 }
628
629 pub fn load_env(self) -> Self {
632 let mut source = PropertySource::new("systemEnvironment");
633 source.set_file_path(PathBuf::from("<env>"));
634
635 for (key, value) in std::env::vars() {
636 let config_key = key.to_lowercase().replace('_', ".");
638 source.put(config_key, Value::string(value));
639 }
640
641 self.config.add_property_source(source);
642 self
643 }
644
645 pub fn load_args(self) -> Self {
648 let args: Vec<String> = std::env::args().collect();
649 let mut source = PropertySource::new("commandLineArgs");
650 source.set_file_path(PathBuf::from("<args>"));
651
652 for arg in args.iter().skip(1) {
653 if let Some((key, value)) = arg.split_once('=')
654 && key.starts_with("--")
655 {
656 let key = key[2..].to_string();
657 source.put(key, Value::string(value));
658 }
659 }
660
661 self.config.add_property_source(source);
662 self
663 }
664
665 pub fn build(mut self) -> ConfigResult<Config> {
668 if self.config.files().is_empty() {
670 self = self.load_defaults();
671 }
672
673 Ok(self.config)
674 }
675
676 fn load_defaults(self) -> Self {
679 let config_dir = ["config", "."];
680 let bases = ["application"];
681 let profiles: Vec<String> = self
682 .config
683 .environment()
684 .get_active_profiles()
685 .iter()
686 .map(|p| p.name().to_string())
687 .collect();
688
689 let formats = [
690 FileFormat::Properties,
691 FileFormat::Yaml,
692 FileFormat::Toml,
693 FileFormat::Json,
694 ];
695
696 let mut builder = self;
697
698 for dir in &config_dir {
700 for base in &bases {
701 for format in &formats {
702 for ext in format.extensions() {
703 let path = PathBuf::from(dir).join(format!("{}.{}", base, ext));
704 if path.exists() {
705 builder = builder.add_file(path);
706 }
707 }
708 }
709 }
710 }
711
712 for profile in &profiles {
714 for dir in &config_dir {
715 for base in &bases {
716 for format in &formats {
717 for ext in format.extensions() {
718 let path =
719 PathBuf::from(dir).join(format!("{}-{}.{}", base, profile, ext));
720 if path.exists() {
721 builder = builder.add_file(path);
722 }
723 }
724 }
725 }
726 }
727 }
728
729 builder
730 }
731}
732
733impl Default for ConfigBuilder {
734 fn default() -> Self {
735 Self::new()
736 }
737}
738
739#[cfg(test)]
740mod tests {
741 use super::*;
742 use crate::{PropertySource, Value};
743 use std::io::Write;
744
745 #[test]
752 fn test_file_format_extensions() {
753 assert_eq!(FileFormat::Properties.extensions(), &["properties", "props"]);
754 assert_eq!(FileFormat::Yaml.extensions(), &["yaml", "yml"]);
755 assert_eq!(FileFormat::Toml.extensions(), &["toml"]);
756 assert_eq!(FileFormat::Json.extensions(), &["json"]);
757 }
758
759 #[test]
762 fn test_file_format_from_path() {
763 assert_eq!(
764 FileFormat::from_path(Path::new("app.properties")),
765 Some(FileFormat::Properties)
766 );
767 assert_eq!(FileFormat::from_path(Path::new("app.props")), Some(FileFormat::Properties));
768 assert_eq!(FileFormat::from_path(Path::new("app.yaml")), Some(FileFormat::Yaml));
769 assert_eq!(FileFormat::from_path(Path::new("app.yml")), Some(FileFormat::Yaml));
770 assert_eq!(FileFormat::from_path(Path::new("app.toml")), Some(FileFormat::Toml));
771 assert_eq!(FileFormat::from_path(Path::new("app.json")), Some(FileFormat::Json));
772 assert_eq!(FileFormat::from_path(Path::new("app.txt")), None);
773 assert_eq!(FileFormat::from_path(Path::new("noext")), None);
774 }
775
776 #[test]
783 fn test_reload_strategy_eq() {
784 assert_eq!(ReloadStrategy::Never, ReloadStrategy::Never);
785 assert_eq!(ReloadStrategy::OnRequest, ReloadStrategy::OnRequest);
786 assert_eq!(ReloadStrategy::Periodic(30), ReloadStrategy::Periodic(30));
787 assert_eq!(ReloadStrategy::Watch, ReloadStrategy::Watch);
788 assert_ne!(ReloadStrategy::Never, ReloadStrategy::Watch);
789 }
790
791 #[test]
798 fn test_config_new() {
799 let config = Config::new();
800 assert!(config.get("nonexistent").is_none());
801 assert!(!config.contains_key("anything"));
802 assert!(config.files().is_empty());
803 assert_eq!(config.reload_strategy(), ReloadStrategy::Never);
804 }
805
806 #[test]
809 fn test_config_default() {
810 let config = Config::default();
811 assert!(config.files().is_empty());
812 }
813
814 #[test]
817 fn test_config_add_source_and_get() {
818 let config = Config::new();
819 let mut source = PropertySource::new("test");
820 source.put("app.name", Value::string("hiver"));
821 source.put("app.port", Value::integer(8080));
822 config.add_property_source(source);
823
824 assert_eq!(config.get("app.name").unwrap().as_str(), Some("hiver"));
825 assert_eq!(config.get("app.port").unwrap().as_i64(), Some(8080));
826 assert!(config.contains_key("app.name"));
827 assert!(!config.contains_key("missing"));
828 }
829
830 #[test]
833 fn test_config_get_as() {
834 let config = Config::new();
835 let mut source = PropertySource::new("test");
836 source.put("count", Value::integer(10));
837 config.add_property_source(source);
838
839 let val: i64 = config.get_as("count").unwrap();
840 assert_eq!(val, 10);
841 }
842
843 #[test]
846 fn test_config_get_as_missing() {
847 let config = Config::new();
848 let result: Result<String, _> = config.get_as("missing");
849 assert!(result.is_err());
850 }
851
852 #[test]
855 fn test_config_get_required() {
856 let config = Config::new();
857 let mut source = PropertySource::new("test");
858 source.put("present", Value::string("value"));
859 config.add_property_source(source);
860
861 assert!(config.get_required("present").is_ok());
862 assert!(config.get_required("absent").is_err());
863 }
864
865 #[test]
868 fn test_config_get_required_as() {
869 let config = Config::new();
870 let mut source = PropertySource::new("test");
871 source.put("enabled", Value::bool(true));
872 config.add_property_source(source);
873
874 let val: bool = config.get_required_as("enabled").unwrap();
875 assert!(val);
876 }
877
878 #[test]
881 fn test_config_get_or() {
882 let config = Config::new();
883 let val = config.get_or("missing", 999i32);
885 assert_eq!(val, 999);
886
887 let mut source = PropertySource::new("test");
889 source.put("found", Value::integer(42));
890 config.add_property_source(source);
891 let val = config.get_or("found", 999i32);
892 assert_eq!(val, 42);
893 }
894
895 #[test]
898 fn test_config_get_prefix() {
899 let config = Config::new();
900 let mut source = PropertySource::new("test");
901 source.put("server.host", Value::string("localhost"));
902 source.put("server.port", Value::integer(8080));
903 source.put("db.url", Value::string("postgres://localhost"));
904 config.add_property_source(source);
905
906 let server_props = config.get_prefix("server.");
907 assert_eq!(server_props.len(), 2);
908 assert!(server_props.contains_key("server.host"));
909 assert!(server_props.contains_key("server.port"));
910
911 let db_props = config.get_prefix("db.");
912 assert_eq!(db_props.len(), 1);
913 }
914
915 #[test]
918 fn test_config_environment() {
919 let config = Config::new();
920 let env = config.environment();
921 assert!(env.get_active_profiles().len() >= 1);
922 }
923
924 #[test]
931 fn test_parse_properties_file() {
932 let dir = tempfile::tempdir().unwrap();
933 let file_path = dir.path().join("test.properties");
934 let mut f = std::fs::File::create(&file_path).unwrap();
935 writeln!(f, "# comment line").unwrap();
936 writeln!(f, "! another comment").unwrap();
937 writeln!(f, "server.host=localhost").unwrap();
938 writeln!(f, "server.port=8080").unwrap();
939 writeln!(f, "").unwrap();
940 writeln!(f, "app.name=hiver").unwrap();
941
942 let config = Config::from_file(&file_path).unwrap();
943 assert_eq!(config.get("server.host").unwrap().as_str(), Some("localhost"));
944 assert_eq!(config.get("server.port").unwrap().as_str(), Some("8080"));
945 assert_eq!(config.get("app.name").unwrap().as_str(), Some("hiver"));
946 }
947
948 #[test]
951 fn test_parse_json_file() {
952 let dir = tempfile::tempdir().unwrap();
953 let file_path = dir.path().join("test.json");
954 let mut f = std::fs::File::create(&file_path).unwrap();
955 write!(f, r#"{{"server": {{"host": "0.0.0.0", "port": 9090}}, "debug": true}}"#).unwrap();
956
957 let config = Config::from_file(&file_path).unwrap();
958 assert!(config.get("server").is_some());
960 assert!(config.get("debug").is_some());
961 assert_eq!(config.get("debug").unwrap().as_bool(), Some(true));
962 }
963
964 #[test]
967 fn test_parse_toml_file() {
968 let dir = tempfile::tempdir().unwrap();
969 let file_path = dir.path().join("test.toml");
970 let mut f = std::fs::File::create(&file_path).unwrap();
971 write!(
972 f,
973 "[server]\nhost = \"localhost\"\nport = 3000\n\n[database]\nurl = \"postgres://db\"\n"
974 )
975 .unwrap();
976
977 let config = Config::from_file(&file_path).unwrap();
978 assert!(config.get("server").is_some());
979 assert!(config.get("database").is_some());
980 }
981
982 #[test]
985 fn test_parse_yaml_file() {
986 let dir = tempfile::tempdir().unwrap();
987 let file_path = dir.path().join("test.yaml");
988 let mut f = std::fs::File::create(&file_path).unwrap();
989 write!(f, "server:\n host: 127.0.0.1\n port: 4000\nlogging:\n level: info\n").unwrap();
990
991 let config = Config::from_file(&file_path).unwrap();
992 assert!(config.get("server").is_some());
993 assert!(config.get("logging").is_some());
994 }
995
996 #[test]
999 fn test_parse_unknown_format() {
1000 let dir = tempfile::tempdir().unwrap();
1001 let file_path = dir.path().join("test.txt");
1002 let _f = std::fs::File::create(&file_path).unwrap();
1003
1004 let result = Config::from_file(&file_path);
1005 assert!(result.is_ok());
1006 }
1007
1008 #[test]
1011 fn test_parse_nonexistent_file() {
1012 let result = Config::from_file("/nonexistent/path/config.yaml");
1013 assert!(result.is_ok());
1014 }
1015
1016 #[test]
1019 fn test_unescape_value() {
1020 assert_eq!(Config::unescape_value("hello\\nworld"), "hello\nworld");
1021 assert_eq!(Config::unescape_value("tab\\there"), "tab\there");
1022 assert_eq!(Config::unescape_value("cr\\rhere"), "cr\rhere");
1023 assert_eq!(Config::unescape_value("back\\slash"), "backslash");
1024 assert_eq!(Config::unescape_value("end\\"), "end\\");
1025 }
1026
1027 #[test]
1034 fn test_builder_add_property() {
1035 let config = Config::builder()
1036 .add_property("key1", "value1")
1037 .add_property("key2", 42)
1038 .build()
1039 .unwrap();
1040
1041 assert_eq!(config.get("key1").unwrap().as_str(), Some("value1"));
1042 assert_eq!(config.get("key2").unwrap().as_i64(), Some(42));
1043 }
1044
1045 #[test]
1048 fn test_builder_add_property_source() {
1049 let mut source = PropertySource::new("custom");
1050 source.put("custom.key", Value::string("custom_value"));
1051
1052 let config = Config::builder()
1053 .add_property_source(source)
1054 .build()
1055 .unwrap();
1056
1057 assert_eq!(config.get("custom.key").unwrap().as_str(), Some("custom_value"));
1058 }
1059
1060 #[test]
1063 fn test_builder_reload_strategy() {
1064 let config = Config::builder()
1065 .reload_strategy(ReloadStrategy::OnRequest)
1066 .build()
1067 .unwrap();
1068
1069 assert_eq!(config.reload_strategy(), ReloadStrategy::OnRequest);
1070 }
1071
1072 #[test]
1075 fn test_builder_default() {
1076 let config = ConfigBuilder::default().build().unwrap();
1077 assert_eq!(config.reload_strategy(), ReloadStrategy::Never);
1078 }
1079
1080 #[test]
1083 fn test_config_multiple_sources_merge() {
1084 let config = Config::new();
1085
1086 let mut source1 = PropertySource::new("first");
1087 source1.put("shared", Value::string("from_first"));
1088 source1.put("only_first", Value::string("yes"));
1089 config.add_property_source(source1);
1090
1091 let mut source2 = PropertySource::new("second");
1092 source2.put("shared", Value::string("from_second"));
1093 source2.put("only_second", Value::string("yes"));
1094 config.add_property_source(source2);
1095
1096 assert_eq!(config.get("shared").unwrap().as_str(), Some("from_first"));
1098 assert_eq!(config.get("only_first").unwrap().as_str(), Some("yes"));
1099 assert_eq!(config.get("only_second").unwrap().as_str(), Some("yes"));
1100 }
1101
1102 #[test]
1103 fn test_add_property_source_first() {
1104 let config = Config::new();
1105
1106 let mut source = PropertySource::new("s1");
1107 source.put("key", Value::string("v1"));
1108 config.add_property_source(source);
1109
1110 assert_eq!(config.get("key").unwrap().as_str(), Some("v1"));
1112
1113 let mut source2 = PropertySource::new("s2");
1115 source2.put("key", Value::string("v2"));
1116 config.add_property_source_first(source2);
1117
1118 assert_eq!(config.get("key").unwrap().as_str(), Some("v2"));
1120 }
1121}