1use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8use std::sync::{Arc, RwLock};
9
10use crate::error::{Error, Result};
11use crate::interpolation::{self, Interpolation, InterpolationArg};
12use crate::resolver::{ResolvedValue, ResolverContext, ResolverRegistry};
13use crate::value::Value;
14
15#[derive(Debug, Clone, Default)]
17pub struct ConfigOptions {
18 pub base_path: Option<PathBuf>,
20 pub allow_http: bool,
22 pub http_allowlist: Vec<String>,
24 pub file_roots: Vec<PathBuf>,
26}
27
28pub struct Config {
33 raw: Arc<Value>,
35 cache: Arc<RwLock<HashMap<String, ResolvedValue>>>,
37 resolvers: Arc<ResolverRegistry>,
39 options: ConfigOptions,
41}
42
43impl Config {
44 pub fn new(value: Value) -> Self {
46 Self {
47 raw: Arc::new(value),
48 cache: Arc::new(RwLock::new(HashMap::new())),
49 resolvers: Arc::new(ResolverRegistry::with_builtins()),
50 options: ConfigOptions::default(),
51 }
52 }
53
54 pub fn with_options(value: Value, options: ConfigOptions) -> Self {
56 Self {
57 raw: Arc::new(value),
58 cache: Arc::new(RwLock::new(HashMap::new())),
59 resolvers: Arc::new(ResolverRegistry::with_builtins()),
60 options,
61 }
62 }
63
64 pub fn with_resolvers(value: Value, resolvers: ResolverRegistry) -> Self {
66 Self {
67 raw: Arc::new(value),
68 cache: Arc::new(RwLock::new(HashMap::new())),
69 resolvers: Arc::new(resolvers),
70 options: ConfigOptions::default(),
71 }
72 }
73
74 pub fn from_yaml(yaml: &str) -> Result<Self> {
76 let value: Value = serde_yaml::from_str(yaml).map_err(|e| Error::parse(e.to_string()))?;
77 Ok(Self::new(value))
78 }
79
80 pub fn from_yaml_with_options(yaml: &str, options: ConfigOptions) -> Result<Self> {
82 let value: Value = serde_yaml::from_str(yaml).map_err(|e| Error::parse(e.to_string()))?;
83 Ok(Self::with_options(value, options))
84 }
85
86 pub fn from_yaml_file(path: impl AsRef<Path>) -> Result<Self> {
88 let path = path.as_ref();
89 let content = std::fs::read_to_string(path).map_err(|e| {
90 Error::parse(format!("Failed to read file '{}': {}", path.display(), e))
91 })?;
92
93 let value: Value =
94 serde_yaml::from_str(&content).map_err(|e| Error::parse(e.to_string()))?;
95
96 let mut options = ConfigOptions::default();
97 options.base_path = path.parent().map(|p| p.to_path_buf());
98
99 Ok(Self::with_options(value, options))
100 }
101
102 pub fn load_merged<P: AsRef<Path>>(paths: &[P]) -> Result<Self> {
111 if paths.is_empty() {
112 return Ok(Self::new(Value::Mapping(indexmap::IndexMap::new())));
113 }
114
115 let mut merged_value: Option<Value> = None;
116 let mut last_base_path: Option<PathBuf> = None;
117
118 for path in paths {
119 let path = path.as_ref();
120 let content = std::fs::read_to_string(path)
121 .map_err(|_e| Error::file_not_found(path.display().to_string(), None))?;
122
123 let value: Value =
124 serde_yaml::from_str(&content).map_err(|e| Error::parse(e.to_string()))?;
125
126 last_base_path = path.parent().map(|p| p.to_path_buf());
127
128 match &mut merged_value {
129 Some(base) => base.merge(value),
130 None => merged_value = Some(value),
131 }
132 }
133
134 let mut options = ConfigOptions::default();
135 options.base_path = last_base_path;
136
137 Ok(Self::with_options(
138 merged_value.unwrap_or(Value::Mapping(indexmap::IndexMap::new())),
139 options,
140 ))
141 }
142
143 pub fn merge(&mut self, other: Config) {
147 if let Some(raw) = Arc::get_mut(&mut self.raw) {
149 raw.merge((*other.raw).clone());
150 } else {
151 let mut new_raw = (*self.raw).clone();
153 new_raw.merge((*other.raw).clone());
154 self.raw = Arc::new(new_raw);
155 }
156 self.clear_cache();
158 }
159
160 pub fn from_json(json: &str) -> Result<Self> {
162 let value: Value = serde_json::from_str(json).map_err(|e| Error::parse(e.to_string()))?;
163 Ok(Self::new(value))
164 }
165
166 pub fn get_raw(&self, path: &str) -> Result<&Value> {
168 self.raw.get_path(path)
169 }
170
171 pub fn get(&self, path: &str) -> Result<Value> {
176 {
178 let cache = self.cache.read().unwrap();
179 if let Some(cached) = cache.get(path) {
180 return Ok(cached.value.clone());
181 }
182 }
183
184 let raw_value = self.raw.get_path(path)?;
186
187 let resolved = self.resolve_value(raw_value, path)?;
189
190 {
192 let mut cache = self.cache.write().unwrap();
193 cache.insert(path.to_string(), resolved.clone());
194 }
195
196 Ok(resolved.value)
197 }
198
199 pub fn get_string(&self, path: &str) -> Result<String> {
201 let value = self.get(path)?;
202 match value {
203 Value::String(s) => Ok(s),
204 Value::Integer(i) => Ok(i.to_string()),
205 Value::Float(f) => Ok(f.to_string()),
206 Value::Bool(b) => Ok(b.to_string()),
207 Value::Null => Ok("null".to_string()),
208 _ => Err(Error::type_coercion(path, "string", value.type_name())),
209 }
210 }
211
212 pub fn get_i64(&self, path: &str) -> Result<i64> {
214 let value = self.get(path)?;
215 match value {
216 Value::Integer(i) => Ok(i),
217 Value::String(s) => s
218 .parse()
219 .map_err(|_| Error::type_coercion(path, "integer", format!("string (\"{}\")", s))),
220 _ => Err(Error::type_coercion(path, "integer", value.type_name())),
221 }
222 }
223
224 pub fn get_f64(&self, path: &str) -> Result<f64> {
226 let value = self.get(path)?;
227 match value {
228 Value::Float(f) => Ok(f),
229 Value::Integer(i) => Ok(i as f64),
230 Value::String(s) => s
231 .parse()
232 .map_err(|_| Error::type_coercion(path, "float", format!("string (\"{}\")", s))),
233 _ => Err(Error::type_coercion(path, "float", value.type_name())),
234 }
235 }
236
237 pub fn get_bool(&self, path: &str) -> Result<bool> {
239 let value = self.get(path)?;
240 match value {
241 Value::Bool(b) => Ok(b),
242 Value::String(s) => {
243 match s.to_lowercase().as_str() {
245 "true" => Ok(true),
246 "false" => Ok(false),
247 _ => Err(Error::type_coercion(
248 path,
249 "boolean",
250 format!("string (\"{}\") - only \"true\" or \"false\" allowed", s),
251 )),
252 }
253 }
254 _ => Err(Error::type_coercion(path, "boolean", value.type_name())),
255 }
256 }
257
258 pub fn resolve_all(&self) -> Result<()> {
260 self.resolve_value_recursive(&self.raw, "")?;
261 Ok(())
262 }
263
264 pub fn to_value(&self) -> Result<Value> {
266 self.resolve_value_to_value(&self.raw, "")
267 }
268
269 pub fn to_value_raw(&self) -> Value {
273 (*self.raw).clone()
274 }
275
276 pub fn to_value_redacted(&self, redact: bool) -> Result<Value> {
280 if redact {
281 self.resolve_value_to_value_redacted(&self.raw, "")
282 } else {
283 self.resolve_value_to_value(&self.raw, "")
284 }
285 }
286
287 pub fn to_yaml(&self) -> Result<String> {
291 let value = self.to_value()?;
292 serde_yaml::to_string(&value).map_err(|e| Error::parse(e.to_string()))
293 }
294
295 pub fn to_yaml_raw(&self) -> Result<String> {
299 serde_yaml::to_string(&*self.raw).map_err(|e| Error::parse(e.to_string()))
300 }
301
302 pub fn to_yaml_redacted(&self, redact: bool) -> Result<String> {
306 let value = self.to_value_redacted(redact)?;
307 serde_yaml::to_string(&value).map_err(|e| Error::parse(e.to_string()))
308 }
309
310 pub fn to_json(&self) -> Result<String> {
314 let value = self.to_value()?;
315 serde_json::to_string_pretty(&value).map_err(|e| Error::parse(e.to_string()))
316 }
317
318 pub fn to_json_raw(&self) -> Result<String> {
322 serde_json::to_string_pretty(&*self.raw).map_err(|e| Error::parse(e.to_string()))
323 }
324
325 pub fn to_json_redacted(&self, redact: bool) -> Result<String> {
329 let value = self.to_value_redacted(redact)?;
330 serde_json::to_string_pretty(&value).map_err(|e| Error::parse(e.to_string()))
331 }
332
333 pub fn clear_cache(&self) {
335 let mut cache = self.cache.write().unwrap();
336 cache.clear();
337 }
338
339 pub fn register_resolver(&mut self, resolver: Arc<dyn crate::resolver::Resolver>) {
341 if let Some(registry) = Arc::get_mut(&mut self.resolvers) {
344 registry.register(resolver);
345 }
346 }
347
348 pub fn validate_raw(&self, schema: &crate::schema::Schema) -> Result<()> {
355 schema.validate(&self.raw)
356 }
357
358 pub fn validate(&self, schema: &crate::schema::Schema) -> Result<()> {
364 let resolved = self.to_value()?;
365 schema.validate(&resolved)
366 }
367
368 pub fn validate_collect(
370 &self,
371 schema: &crate::schema::Schema,
372 ) -> Vec<crate::schema::ValidationError> {
373 match self.to_value() {
374 Ok(resolved) => schema.validate_collect(&resolved),
375 Err(e) => vec![crate::schema::ValidationError {
376 path: String::new(),
377 message: e.to_string(),
378 }],
379 }
380 }
381
382 fn resolve_value(&self, value: &Value, path: &str) -> Result<ResolvedValue> {
384 match value {
385 Value::String(s) => {
386 if interpolation::needs_processing(s) {
388 let parsed = interpolation::parse(s)?;
389 self.resolve_interpolation(&parsed, path)
390 } else {
391 Ok(ResolvedValue::new(value.clone()))
392 }
393 }
394 _ => Ok(ResolvedValue::new(value.clone())),
395 }
396 }
397
398 fn resolve_interpolation(&self, interp: &Interpolation, path: &str) -> Result<ResolvedValue> {
400 match interp {
401 Interpolation::Literal(s) => Ok(ResolvedValue::new(Value::String(s.clone()))),
402
403 Interpolation::Resolver { name, args, kwargs } => {
404 let mut ctx = ResolverContext::new(path);
406 ctx.config_root = Some(Arc::clone(&self.raw));
407 if let Some(base) = &self.options.base_path {
408 ctx.base_path = Some(base.clone());
409 }
410
411 let resolved_args: Vec<String> = args
413 .iter()
414 .map(|arg| self.resolve_arg(arg, path))
415 .collect::<Result<Vec<_>>>()?;
416
417 let resolved_kwargs: HashMap<String, String> = kwargs
418 .iter()
419 .map(|(k, v)| Ok((k.clone(), self.resolve_arg(v, path)?)))
420 .collect::<Result<HashMap<_, _>>>()?;
421
422 self.resolvers
424 .resolve(name, &resolved_args, &resolved_kwargs, &ctx)
425 }
426
427 Interpolation::SelfRef {
428 path: ref_path,
429 relative,
430 } => {
431 let full_path = if *relative {
432 self.resolve_relative_path(path, ref_path)
433 } else {
434 ref_path.clone()
435 };
436
437 if full_path == path {
440 return Err(Error::circular_reference(
441 path,
442 vec![path.to_string(), full_path],
443 ));
444 }
445
446 let ref_value = self
448 .raw
449 .get_path(&full_path)
450 .map_err(|_| Error::ref_not_found(&full_path, Some(path.to_string())))?;
451
452 self.resolve_value(ref_value, &full_path)
454 }
455
456 Interpolation::Concat(parts) => {
457 let mut result = String::new();
458 let mut any_sensitive = false;
459
460 for part in parts {
461 let resolved = self.resolve_interpolation(part, path)?;
462 any_sensitive = any_sensitive || resolved.sensitive;
463
464 match resolved.value {
465 Value::String(s) => result.push_str(&s),
466 other => result.push_str(&other.to_string()),
467 }
468 }
469
470 if any_sensitive {
471 Ok(ResolvedValue::sensitive(Value::String(result)))
472 } else {
473 Ok(ResolvedValue::new(Value::String(result)))
474 }
475 }
476 }
477 }
478
479 fn resolve_arg(&self, arg: &InterpolationArg, path: &str) -> Result<String> {
481 match arg {
482 InterpolationArg::Literal(s) => Ok(s.clone()),
483 InterpolationArg::Nested(interp) => {
484 let resolved = self.resolve_interpolation(interp, path)?;
485 match resolved.value {
486 Value::String(s) => Ok(s),
487 other => Ok(other.to_string()),
488 }
489 }
490 }
491 }
492
493 fn resolve_relative_path(&self, current_path: &str, ref_path: &str) -> String {
495 let mut ref_chars = ref_path.chars().peekable();
496 let mut levels_up = 0;
497
498 while ref_chars.peek() == Some(&'.') {
500 ref_chars.next();
501 levels_up += 1;
502 }
503
504 let remaining: String = ref_chars.collect();
506
507 if levels_up == 0 {
508 return ref_path.to_string();
510 }
511
512 let mut segments: Vec<&str> = current_path.split('.').collect();
514
515 for _ in 0..levels_up {
519 segments.pop();
520 }
521
522 if remaining.is_empty() {
524 segments.join(".")
525 } else if segments.is_empty() {
526 remaining
527 } else {
528 format!("{}.{}", segments.join("."), remaining)
529 }
530 }
531
532 fn resolve_value_recursive(&self, value: &Value, path: &str) -> Result<ResolvedValue> {
534 match value {
535 Value::String(s) => {
536 if interpolation::needs_processing(s) {
537 let parsed = interpolation::parse(s)?;
538 let resolved = self.resolve_interpolation(&parsed, path)?;
539
540 let mut cache = self.cache.write().unwrap();
542 cache.insert(path.to_string(), resolved.clone());
543
544 Ok(resolved)
545 } else {
546 Ok(ResolvedValue::new(value.clone()))
547 }
548 }
549 Value::Sequence(seq) => {
550 for (i, item) in seq.iter().enumerate() {
551 let item_path = format!("{}[{}]", path, i);
552 self.resolve_value_recursive(item, &item_path)?;
553 }
554 Ok(ResolvedValue::new(value.clone()))
555 }
556 Value::Mapping(map) => {
557 for (key, val) in map {
558 let key_path = if path.is_empty() {
559 key.clone()
560 } else {
561 format!("{}.{}", path, key)
562 };
563 self.resolve_value_recursive(val, &key_path)?;
564 }
565 Ok(ResolvedValue::new(value.clone()))
566 }
567 _ => Ok(ResolvedValue::new(value.clone())),
568 }
569 }
570
571 fn resolve_value_to_value(&self, value: &Value, path: &str) -> Result<Value> {
573 match value {
574 Value::String(s) => {
575 if interpolation::needs_processing(s) {
576 let parsed = interpolation::parse(s)?;
577 let resolved = self.resolve_interpolation(&parsed, path)?;
578 Ok(resolved.value)
579 } else {
580 Ok(value.clone())
581 }
582 }
583 Value::Sequence(seq) => {
584 let resolved: Result<Vec<Value>> = seq
585 .iter()
586 .enumerate()
587 .map(|(i, item)| {
588 let item_path = format!("{}[{}]", path, i);
589 self.resolve_value_to_value(item, &item_path)
590 })
591 .collect();
592 Ok(Value::Sequence(resolved?))
593 }
594 Value::Mapping(map) => {
595 let mut resolved = indexmap::IndexMap::new();
596 for (key, val) in map {
597 let key_path = if path.is_empty() {
598 key.clone()
599 } else {
600 format!("{}.{}", path, key)
601 };
602 resolved.insert(key.clone(), self.resolve_value_to_value(val, &key_path)?);
603 }
604 Ok(Value::Mapping(resolved))
605 }
606 _ => Ok(value.clone()),
607 }
608 }
609
610 fn resolve_value_to_value_redacted(&self, value: &Value, path: &str) -> Result<Value> {
612 const REDACTED: &str = "[REDACTED]";
613
614 match value {
615 Value::String(s) => {
616 if interpolation::needs_processing(s) {
617 let parsed = interpolation::parse(s)?;
618 let resolved = self.resolve_interpolation(&parsed, path)?;
619 if resolved.sensitive {
620 Ok(Value::String(REDACTED.to_string()))
621 } else {
622 Ok(resolved.value)
623 }
624 } else {
625 Ok(value.clone())
626 }
627 }
628 Value::Sequence(seq) => {
629 let resolved: Result<Vec<Value>> = seq
630 .iter()
631 .enumerate()
632 .map(|(i, item)| {
633 let item_path = format!("{}[{}]", path, i);
634 self.resolve_value_to_value_redacted(item, &item_path)
635 })
636 .collect();
637 Ok(Value::Sequence(resolved?))
638 }
639 Value::Mapping(map) => {
640 let mut resolved = indexmap::IndexMap::new();
641 for (key, val) in map {
642 let key_path = if path.is_empty() {
643 key.clone()
644 } else {
645 format!("{}.{}", path, key)
646 };
647 resolved.insert(
648 key.clone(),
649 self.resolve_value_to_value_redacted(val, &key_path)?,
650 );
651 }
652 Ok(Value::Mapping(resolved))
653 }
654 _ => Ok(value.clone()),
655 }
656 }
657}
658
659impl Clone for Config {
660 fn clone(&self) -> Self {
661 Self {
662 raw: Arc::clone(&self.raw),
663 cache: Arc::new(RwLock::new(HashMap::new())), resolvers: Arc::clone(&self.resolvers),
665 options: self.options.clone(),
666 }
667 }
668}
669
670#[cfg(test)]
671mod tests {
672 use super::*;
673
674 #[test]
675 fn test_load_yaml() {
676 let yaml = r#"
677database:
678 host: localhost
679 port: 5432
680"#;
681 let config = Config::from_yaml(yaml).unwrap();
682
683 assert_eq!(
684 config.get("database.host").unwrap().as_str(),
685 Some("localhost")
686 );
687 assert_eq!(config.get("database.port").unwrap().as_i64(), Some(5432));
688 }
689
690 #[test]
691 fn test_env_resolver() {
692 std::env::set_var("HOLOCONF_TEST_HOST", "prod-server");
693
694 let yaml = r#"
695server:
696 host: ${env:HOLOCONF_TEST_HOST}
697"#;
698 let config = Config::from_yaml(yaml).unwrap();
699
700 assert_eq!(
701 config.get("server.host").unwrap().as_str(),
702 Some("prod-server")
703 );
704
705 std::env::remove_var("HOLOCONF_TEST_HOST");
706 }
707
708 #[test]
709 fn test_env_resolver_with_default() {
710 std::env::remove_var("HOLOCONF_MISSING_VAR");
711
712 let yaml = r#"
713server:
714 host: ${env:HOLOCONF_MISSING_VAR,default-host}
715"#;
716 let config = Config::from_yaml(yaml).unwrap();
717
718 assert_eq!(
719 config.get("server.host").unwrap().as_str(),
720 Some("default-host")
721 );
722 }
723
724 #[test]
725 fn test_self_reference() {
726 let yaml = r#"
727defaults:
728 host: localhost
729database:
730 host: ${defaults.host}
731"#;
732 let config = Config::from_yaml(yaml).unwrap();
733
734 assert_eq!(
735 config.get("database.host").unwrap().as_str(),
736 Some("localhost")
737 );
738 }
739
740 #[test]
741 fn test_string_concatenation() {
742 std::env::set_var("HOLOCONF_PREFIX", "prod");
743
744 let yaml = r#"
745bucket: myapp-${env:HOLOCONF_PREFIX}-data
746"#;
747 let config = Config::from_yaml(yaml).unwrap();
748
749 assert_eq!(
750 config.get("bucket").unwrap().as_str(),
751 Some("myapp-prod-data")
752 );
753
754 std::env::remove_var("HOLOCONF_PREFIX");
755 }
756
757 #[test]
758 fn test_escaped_interpolation() {
759 let yaml = r#"
762literal: '\${not_resolved}'
763"#;
764 let config = Config::from_yaml(yaml).unwrap();
765
766 assert_eq!(
768 config.get("literal").unwrap().as_str(),
769 Some("${not_resolved}")
770 );
771 }
772
773 #[test]
774 fn test_type_coercion_string_to_int() {
775 std::env::set_var("HOLOCONF_PORT", "8080");
776
777 let yaml = r#"
778port: ${env:HOLOCONF_PORT}
779"#;
780 let config = Config::from_yaml(yaml).unwrap();
781
782 assert_eq!(config.get_i64("port").unwrap(), 8080);
784
785 std::env::remove_var("HOLOCONF_PORT");
786 }
787
788 #[test]
789 fn test_strict_boolean_coercion() {
790 std::env::set_var("HOLOCONF_ENABLED", "true");
791 std::env::set_var("HOLOCONF_INVALID", "1");
792
793 let yaml = r#"
794enabled: ${env:HOLOCONF_ENABLED}
795invalid: ${env:HOLOCONF_INVALID}
796"#;
797 let config = Config::from_yaml(yaml).unwrap();
798
799 assert!(config.get_bool("enabled").unwrap());
801
802 assert!(config.get_bool("invalid").is_err());
804
805 std::env::remove_var("HOLOCONF_ENABLED");
806 std::env::remove_var("HOLOCONF_INVALID");
807 }
808
809 #[test]
810 fn test_caching() {
811 std::env::set_var("HOLOCONF_CACHED", "initial");
812
813 let yaml = r#"
814value: ${env:HOLOCONF_CACHED}
815"#;
816 let config = Config::from_yaml(yaml).unwrap();
817
818 assert_eq!(config.get("value").unwrap().as_str(), Some("initial"));
820
821 std::env::set_var("HOLOCONF_CACHED", "changed");
823
824 assert_eq!(config.get("value").unwrap().as_str(), Some("initial"));
826
827 config.clear_cache();
829
830 assert_eq!(config.get("value").unwrap().as_str(), Some("changed"));
832
833 std::env::remove_var("HOLOCONF_CACHED");
834 }
835
836 #[test]
837 fn test_path_not_found() {
838 let yaml = r#"
839database:
840 host: localhost
841"#;
842 let config = Config::from_yaml(yaml).unwrap();
843
844 let result = config.get("database.nonexistent");
845 assert!(result.is_err());
846 }
847
848 #[test]
849 fn test_to_yaml() {
850 std::env::set_var("HOLOCONF_EXPORT_HOST", "exported-host");
851
852 let yaml = r#"
853server:
854 host: ${env:HOLOCONF_EXPORT_HOST}
855 port: 8080
856"#;
857 let config = Config::from_yaml(yaml).unwrap();
858
859 let exported = config.to_yaml().unwrap();
860 assert!(exported.contains("exported-host"));
861 assert!(exported.contains("8080"));
862
863 std::env::remove_var("HOLOCONF_EXPORT_HOST");
864 }
865
866 #[test]
867 fn test_relative_path_sibling() {
868 let yaml = r#"
869database:
870 host: localhost
871 url: postgres://${.host}:5432/db
872"#;
873 let config = Config::from_yaml(yaml).unwrap();
874
875 assert_eq!(
876 config.get("database.url").unwrap().as_str(),
877 Some("postgres://localhost:5432/db")
878 );
879 }
880
881 #[test]
882 fn test_array_access() {
883 let yaml = r#"
884servers:
885 - host: server1
886 - host: server2
887primary: ${servers[0].host}
888"#;
889 let config = Config::from_yaml(yaml).unwrap();
890
891 assert_eq!(config.get("primary").unwrap().as_str(), Some("server1"));
892 }
893
894 #[test]
895 fn test_nested_interpolation() {
896 std::env::set_var("HOLOCONF_DEFAULT_HOST", "fallback-host");
897
898 let yaml = r#"
899host: ${env:UNDEFINED_HOST,${env:HOLOCONF_DEFAULT_HOST}}
900"#;
901 let config = Config::from_yaml(yaml).unwrap();
902
903 assert_eq!(config.get("host").unwrap().as_str(), Some("fallback-host"));
904
905 std::env::remove_var("HOLOCONF_DEFAULT_HOST");
906 }
907
908 #[test]
909 fn test_to_yaml_raw() {
910 let yaml = r#"
911server:
912 host: ${env:MY_HOST}
913 port: 8080
914"#;
915 let config = Config::from_yaml(yaml).unwrap();
916
917 let raw = config.to_yaml_raw().unwrap();
918 assert!(raw.contains("${env:MY_HOST}"));
920 assert!(raw.contains("8080"));
921 }
922
923 #[test]
924 fn test_to_json_raw() {
925 let yaml = r#"
926database:
927 url: ${env:DATABASE_URL}
928"#;
929 let config = Config::from_yaml(yaml).unwrap();
930
931 let raw = config.to_json_raw().unwrap();
932 assert!(raw.contains("${env:DATABASE_URL}"));
934 }
935
936 #[test]
937 fn test_to_value_raw() {
938 let yaml = r#"
939key: ${env:SOME_VAR}
940"#;
941 let config = Config::from_yaml(yaml).unwrap();
942
943 let raw = config.to_value_raw();
944 assert_eq!(
945 raw.get_path("key").unwrap().as_str(),
946 Some("${env:SOME_VAR}")
947 );
948 }
949
950 #[test]
951 fn test_to_yaml_redacted_no_sensitive() {
952 std::env::set_var("HOLOCONF_NON_SENSITIVE", "public-value");
953
954 let yaml = r#"
955value: ${env:HOLOCONF_NON_SENSITIVE}
956"#;
957 let config = Config::from_yaml(yaml).unwrap();
958
959 let output = config.to_yaml_redacted(true).unwrap();
961 assert!(output.contains("public-value"));
962
963 std::env::remove_var("HOLOCONF_NON_SENSITIVE");
964 }
965}