1use super::env::EnvError;
7use super::env_loader::EnvLoader;
8use super::profile::Profile;
9use indexmap::IndexMap;
10use serde_json::Value;
11use std::collections::HashMap;
12use std::fs;
13use std::path::{Path, PathBuf};
14
15pub trait ConfigSource: Send + Sync {
17 fn load(&self) -> Result<IndexMap<String, Value>, SourceError>;
19
20 fn priority(&self) -> u8;
22
23 fn description(&self) -> String;
25}
26
27#[non_exhaustive]
29#[derive(Debug, thiserror::Error)]
30pub enum SourceError {
31 #[error("IO error: {0}")]
33 Io(#[from] std::io::Error),
34
35 #[error("Parse error: {0}")]
37 Parse(String),
38
39 #[error("Environment error: {0}")]
41 Env(#[from] EnvError),
42
43 #[error("TOML error: {0}")]
45 Toml(#[from] toml::de::Error),
46
47 #[error("JSON error: {0}")]
49 Json(#[from] serde_json::Error),
50
51 #[error("Invalid source: {0}")]
53 InvalidSource(String),
54
55 #[error("Interpolation error: {0}")]
61 Interpolation(#[from] Box<super::interpolation::InterpolationError>),
62}
63
64impl From<super::interpolation::InterpolationError> for SourceError {
68 fn from(err: super::interpolation::InterpolationError) -> Self {
69 SourceError::Interpolation(Box::new(err))
70 }
71}
72
73pub struct EnvSource {
75 prefix: Option<String>,
76 interpolate: bool,
77}
78
79impl EnvSource {
80 pub fn new() -> Self {
91 Self {
92 prefix: None,
93 interpolate: false,
94 }
95 }
96 pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
108 self.prefix = Some(prefix.into());
109 self
110 }
111 pub fn with_interpolation(mut self, enabled: bool) -> Self {
123 self.interpolate = enabled;
124 self
125 }
126}
127
128impl Default for EnvSource {
129 fn default() -> Self {
130 Self::new()
131 }
132}
133
134impl ConfigSource for EnvSource {
135 fn load(&self) -> Result<IndexMap<String, Value>, SourceError> {
136 let mut config = IndexMap::new();
137
138 for (key, value) in std::env::vars() {
140 if let Some(prefix) = &self.prefix
142 && !key.starts_with(prefix)
143 {
144 continue;
145 }
146
147 let clean_key = if let Some(prefix) = &self.prefix {
149 key.strip_prefix(prefix).unwrap_or(&key).to_string()
150 } else {
151 key.clone()
152 };
153
154 let lower_key = clean_key.to_lowercase();
156
157 let parsed_value = if lower_key == "debug" {
159 match value.trim().to_lowercase().as_str() {
161 "true" | "1" | "yes" | "on" => Value::Bool(true),
162 "false" | "0" | "no" | "off" => Value::Bool(false),
163 _ => {
164 if let Ok(b) = value.parse::<bool>() {
165 Value::Bool(b)
166 } else {
167 Value::String(value)
168 }
169 }
170 }
171 } else if lower_key == "allowed_hosts" {
172 let list: Vec<_> = value
174 .split(',')
175 .map(|s| Value::String(s.trim().to_string()))
176 .collect();
177 Value::Array(list)
178 } else if let Ok(num) = value.parse::<i64>() {
179 Value::Number(num.into())
180 } else if let Ok(b) = value.parse::<bool>() {
181 Value::Bool(b)
182 } else {
183 Value::String(value)
184 };
185
186 config.insert(lower_key, parsed_value);
187 }
188
189 Ok(config)
190 }
191
192 fn priority(&self) -> u8 {
193 100 }
195
196 fn description(&self) -> String {
197 match &self.prefix {
198 Some(prefix) => format!("Environment variables (prefix: {})", prefix),
199 None => "Environment variables".to_string(),
200 }
201 }
202}
203
204pub struct DotEnvSource {
206 path: Option<PathBuf>,
207 profile: Option<Profile>,
208 interpolate: bool,
209}
210
211impl DotEnvSource {
212 pub fn new() -> Self {
223 Self {
224 path: None,
225 profile: None,
226 interpolate: false,
227 }
228 }
229 pub fn with_path(mut self, path: impl Into<PathBuf>) -> Self {
241 self.path = Some(path.into());
242 self
243 }
244 pub fn with_profile(mut self, profile: Profile) -> Self {
257 self.profile = Some(profile);
258 self
259 }
260 pub fn with_interpolation(mut self, enabled: bool) -> Self {
272 self.interpolate = enabled;
273 self
274 }
275}
276
277impl Default for DotEnvSource {
278 fn default() -> Self {
279 Self::new()
280 }
281}
282
283impl ConfigSource for DotEnvSource {
284 fn load(&self) -> Result<IndexMap<String, Value>, SourceError> {
285 let path = match &self.path {
286 Some(p) => p.clone(),
287 None => {
288 let filename = match &self.profile {
289 Some(profile) => profile.env_file_name(),
290 None => ".env".to_string(),
291 };
292 PathBuf::from(filename)
293 }
294 };
295
296 let loader = EnvLoader::new()
298 .path(&path)
299 .interpolate(self.interpolate)
300 .overwrite(false);
301
302 let _ = loader.load_optional()?;
304
305 Ok(IndexMap::new())
308 }
309
310 fn priority(&self) -> u8 {
311 90 }
313
314 fn description(&self) -> String {
315 match &self.path {
316 Some(path) => format!(".env file: {}", path.display()),
317 None => match &self.profile {
318 Some(profile) => format!(".env file: {}", profile.env_file_name()),
319 None => ".env file".to_string(),
320 },
321 }
322 }
323}
324
325pub struct TomlFileSource {
327 path: PathBuf,
328 interpolate: bool,
329}
330
331impl TomlFileSource {
332 pub fn new(path: impl Into<PathBuf>) -> Self {
352 Self {
353 path: path.into(),
354 interpolate: true,
355 }
356 }
357
358 pub fn with_interpolation(mut self) -> Self {
388 self.interpolate = true;
389 self
390 }
391
392 pub fn without_interpolation(mut self) -> Self {
409 self.interpolate = false;
410 self
411 }
412
413 #[deprecated(
419 since = "0.1.0-rc.27",
420 note = "Use with_interpolation()/without_interpolation() instead; will be removed in 0.2.0 (issue #4224)"
421 )]
422 pub fn set_interpolation(mut self, enabled: bool) -> Self {
423 self.interpolate = enabled;
424 self
425 }
426}
427
428impl ConfigSource for TomlFileSource {
429 fn load(&self) -> Result<IndexMap<String, Value>, SourceError> {
430 if !self.path.exists() {
431 return Ok(IndexMap::new());
432 }
433
434 let content = fs::read_to_string(&self.path)?;
435 let mut toml_value: toml::Value = toml::from_str(&content)?;
436
437 if self.interpolate {
440 let lookup = |name: &str| std::env::var(name).ok();
441 let interpolator = super::interpolation::Interpolator::new(&lookup);
442 interpolator.interpolate_value(&mut toml_value, &self.path)?;
443 }
444
445 let json_str = serde_json::to_string(&toml_value)?;
447 let json_value: Value = serde_json::from_str(&json_str)?;
448
449 let map = json_value
451 .as_object()
452 .ok_or_else(|| SourceError::Parse("Expected object at root".to_string()))?;
453
454 Ok(map.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
455 }
456
457 fn priority(&self) -> u8 {
458 50 }
460
461 fn description(&self) -> String {
462 format!("TOML file: {}", self.path.display())
463 }
464}
465
466#[deprecated(
474 since = "0.1.0-rc.26",
475 note = "Use TomlFileSource instead. JsonFileSource will be removed in 0.2.0 (issue #4087)"
476)]
477pub struct JsonFileSource {
478 path: PathBuf,
479}
480
481#[allow(deprecated)] impl JsonFileSource {
483 #[deprecated(
495 since = "0.1.0-rc.26",
496 note = "Use TomlFileSource::new instead. JsonFileSource will be removed in 0.2.0 (issue #4087)"
497 )]
498 pub fn new(path: impl Into<PathBuf>) -> Self {
499 Self { path: path.into() }
500 }
501}
502
503#[allow(deprecated)] impl ConfigSource for JsonFileSource {
505 fn load(&self) -> Result<IndexMap<String, Value>, SourceError> {
506 if !self.path.exists() {
507 return Ok(IndexMap::new());
508 }
509
510 let content = fs::read_to_string(&self.path)?;
511 let json_value: Value = serde_json::from_str(&content)?;
512
513 let map = json_value
515 .as_object()
516 .ok_or_else(|| SourceError::Parse("Expected object at root".to_string()))?;
517
518 Ok(map.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
519 }
520
521 fn priority(&self) -> u8 {
522 50 }
524
525 fn description(&self) -> String {
526 format!("JSON file: {}", self.path.display())
527 }
528}
529
530pub struct DefaultSource {
532 values: IndexMap<String, Value>,
533}
534
535impl DefaultSource {
536 pub fn new() -> Self {
549 Self {
550 values: IndexMap::new(),
551 }
552 }
553 pub fn with_value(mut self, key: impl Into<String>, value: Value) -> Self {
565 self.values.insert(key.into(), value);
566 self
567 }
568 pub fn with_defaults(mut self, defaults: HashMap<String, Value>) -> Self {
585 self.values.extend(defaults);
586 self
587 }
588}
589
590impl Default for DefaultSource {
591 fn default() -> Self {
592 Self::new()
593 }
594}
595
596impl ConfigSource for DefaultSource {
597 fn load(&self) -> Result<IndexMap<String, Value>, SourceError> {
598 Ok(self.values.clone())
599 }
600
601 fn priority(&self) -> u8 {
602 0 }
604
605 fn description(&self) -> String {
606 "Default values".to_string()
607 }
608}
609#[deprecated(
630 since = "0.1.0-rc.26",
631 note = "Use TomlFileSource::new directly. The *.json branch will be removed in 0.2.0 (issue #4087)"
632)]
633pub fn auto_source(path: impl AsRef<Path>) -> Result<Box<dyn ConfigSource>, SourceError> {
634 let path = path.as_ref();
635 let ext = path
636 .extension()
637 .and_then(|e| e.to_str())
638 .ok_or_else(|| SourceError::InvalidSource("No file extension".to_string()))?;
639
640 match ext {
641 "toml" => Ok(Box::new(TomlFileSource::new(path))),
642 #[allow(deprecated)]
645 "json" => Ok(Box::new(JsonFileSource::new(path))),
646 _ => Err(SourceError::InvalidSource(format!(
647 "Unsupported file extension: {}",
648 ext
649 ))),
650 }
651}
652
653pub struct LowPriorityEnvSource {
672 inner: EnvSource,
673}
674
675impl LowPriorityEnvSource {
676 pub fn new() -> Self {
686 Self {
687 inner: EnvSource::new(),
688 }
689 }
690
691 pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
702 self.inner = self.inner.with_prefix(prefix);
703 self
704 }
705
706 pub fn with_interpolation(mut self, enabled: bool) -> Self {
717 self.inner = self.inner.with_interpolation(enabled);
718 self
719 }
720}
721
722impl Default for LowPriorityEnvSource {
723 fn default() -> Self {
724 Self::new()
725 }
726}
727
728impl ConfigSource for LowPriorityEnvSource {
729 fn load(&self) -> Result<IndexMap<String, Value>, SourceError> {
730 self.inner.load()
731 }
732
733 fn priority(&self) -> u8 {
734 40 }
736
737 fn description(&self) -> String {
738 format!("{} (low priority)", self.inner.description())
739 }
740}
741
742pub struct HighPriorityEnvSource {
763 inner: EnvSource,
764}
765
766impl HighPriorityEnvSource {
767 pub fn new() -> Self {
777 Self {
778 inner: EnvSource::new(),
779 }
780 }
781
782 pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
793 self.inner = self.inner.with_prefix(prefix);
794 self
795 }
796
797 pub fn with_interpolation(mut self, enabled: bool) -> Self {
808 self.inner = self.inner.with_interpolation(enabled);
809 self
810 }
811}
812
813impl Default for HighPriorityEnvSource {
814 fn default() -> Self {
815 Self::new()
816 }
817}
818
819impl ConfigSource for HighPriorityEnvSource {
820 fn load(&self) -> Result<IndexMap<String, Value>, SourceError> {
821 self.inner.load()
822 }
823
824 fn priority(&self) -> u8 {
825 60 }
827
828 fn description(&self) -> String {
829 format!("{} (high priority)", self.inner.description())
830 }
831}
832
833#[cfg(test)]
834mod tests {
835 use super::*;
836 use std::env;
837 use std::fs::File;
838 use std::io::Write;
839 use tempfile::TempDir;
840
841 #[test]
842 fn test_env_source() {
843 unsafe {
846 env::set_var("SECRET_KEY", "test-secret");
847 env::set_var("DEBUG", "true");
848 }
849
850 let source = EnvSource::new();
851 let config = source.load().unwrap();
852
853 assert_eq!(
854 config.get("secret_key").unwrap(),
855 &Value::String("test-secret".to_string())
856 );
857 assert_eq!(config.get("debug").unwrap(), &Value::Bool(true));
858
859 unsafe {
862 env::remove_var("SECRET_KEY");
863 env::remove_var("DEBUG");
864 }
865 }
866
867 #[test]
868 fn test_toml_source() {
869 let temp_dir = TempDir::new().unwrap();
870 let config_path = temp_dir.path().join("config.toml");
871
872 let mut file = File::create(&config_path).unwrap();
873 writeln!(
874 file,
875 r#"
876debug = true
877secret_key = "test-key"
878 "#
879 )
880 .unwrap();
881
882 let source = TomlFileSource::new(&config_path);
883 let config = source.load().unwrap();
884
885 assert_eq!(config.get("debug").unwrap(), &Value::Bool(true));
886 assert_eq!(
887 config.get("secret_key").unwrap(),
888 &Value::String("test-key".to_string())
889 );
890 }
891
892 #[allow(deprecated)]
895 #[test]
896 fn test_json_source() {
897 let temp_dir = TempDir::new().unwrap();
898 let config_path = temp_dir.path().join("config.json");
899
900 let mut file = File::create(&config_path).unwrap();
901 writeln!(
902 file,
903 r#"{{
904 "debug": false,
905 "secret_key": "json-key"
906 }}"#
907 )
908 .unwrap();
909
910 let source = JsonFileSource::new(&config_path);
911 let config = source.load().unwrap();
912
913 assert_eq!(config.get("debug").unwrap(), &Value::Bool(false));
914 assert_eq!(
915 config.get("secret_key").unwrap(),
916 &Value::String("json-key".to_string())
917 );
918 }
919
920 #[test]
921 fn test_default_source() {
922 let source = DefaultSource::new()
923 .with_value("key1", Value::String("value1".to_string()))
924 .with_value("key2", Value::Bool(true));
925
926 let config = source.load().unwrap();
927
928 assert_eq!(
929 config.get("key1").unwrap(),
930 &Value::String("value1".to_string())
931 );
932 assert_eq!(config.get("key2").unwrap(), &Value::Bool(true));
933 }
934
935 #[test]
936 fn test_source_priority() {
937 assert_eq!(EnvSource::new().priority(), 100);
938 assert_eq!(DotEnvSource::new().priority(), 90);
939 assert_eq!(HighPriorityEnvSource::new().priority(), 60);
940 assert_eq!(TomlFileSource::new("test.toml").priority(), 50);
941 assert_eq!(LowPriorityEnvSource::new().priority(), 40);
942 assert_eq!(DefaultSource::new().priority(), 0);
943 }
944
945 #[test]
946 fn test_high_priority_env_source_wraps_env_source() {
947 let source = HighPriorityEnvSource::new();
949
950 let priority = source.priority();
952 let description = source.description();
953
954 assert_eq!(priority, 60);
956 assert!(description.contains("high priority"));
957 }
958
959 #[test]
960 fn test_high_priority_env_source_with_prefix() {
961 let source = HighPriorityEnvSource::new().with_prefix("REINHARDT_TEST_");
963
964 let description = source.description();
966
967 assert!(description.contains("REINHARDT_TEST_"));
969 assert!(description.contains("high priority"));
970 }
971
972 #[test]
973 fn toml_file_source_without_interpolation_preserves_literal() {
974 let temp_dir = TempDir::new().unwrap();
976 let config_path = temp_dir.path().join("config.toml");
977 let mut file = File::create(&config_path).unwrap();
978 writeln!(file, r#"host = "${{LITERAL_VAR}}""#).unwrap();
979
980 let source = TomlFileSource::new(&config_path).without_interpolation();
982 let config = source.load().unwrap();
983
984 assert_eq!(
986 config.get("host").unwrap(),
987 &Value::String("${LITERAL_VAR}".to_string())
988 );
989 }
990
991 #[test]
992 fn test_high_priority_env_source_overrides_toml() {
993 let temp_dir = TempDir::new().unwrap();
995 let config_path = temp_dir.path().join("config.toml");
996 let mut file = File::create(&config_path).unwrap();
997 writeln!(file, r#"port = 1025"#).unwrap();
998
999 let prefix = "HPENV_TEST_3518_";
1000 let env_key = format!("{prefix}PORT");
1001
1002 unsafe { env::set_var(&env_key, "9999") };
1004
1005 let settings = crate::settings::builder::SettingsBuilder::new()
1007 .add_source(TomlFileSource::new(&config_path))
1008 .add_source(HighPriorityEnvSource::new().with_prefix(prefix))
1009 .build()
1010 .unwrap();
1011
1012 let port: i64 = settings.get("port").unwrap();
1014 assert_eq!(port, 9999);
1015
1016 unsafe { env::remove_var(&env_key) };
1018 }
1019}