Skip to main content

rskit_config/source/
env.rs

1use rskit_errors::{AppError, AppResult};
2
3use super::contract::ConfigSource;
4
5/// Environment-variable configuration source.
6#[derive(Debug, Clone, Default)]
7pub struct EnvironmentSource {
8    prefix: String,
9}
10
11impl EnvironmentSource {
12    /// Create an environment source without a prefix.
13    pub fn new() -> Self {
14        Self::default()
15    }
16
17    /// Create an environment source with a prefix.
18    pub fn with_prefix(prefix: impl Into<String>) -> Self {
19        Self {
20            prefix: prefix.into(),
21        }
22    }
23}
24
25impl ConfigSource for EnvironmentSource {
26    fn collect(&self) -> AppResult<config::Config> {
27        let env_source = if self.prefix.is_empty() {
28            config::Environment::default()
29                .separator("__")
30                .try_parsing(true)
31        } else {
32            config::Environment::with_prefix(&self.prefix)
33                .separator("__")
34                .try_parsing(true)
35        };
36
37        config::Config::builder()
38            .add_source(env_source)
39            .build()
40            .map_err(|e| AppError::invalid_input("config", e.to_string()))
41    }
42}
43
44pub(crate) fn normalize_env_key(prefix: &str, key: &str) -> Option<String> {
45    let key = key.to_ascii_lowercase();
46    if prefix.is_empty() {
47        return Some(key.replace("__", "."));
48    }
49
50    let prefix = prefix.to_ascii_lowercase();
51    key.strip_prefix(&format!("{prefix}__"))
52        .map(|stripped| stripped.replace("__", "."))
53}
54
55pub(crate) fn parse_env_value(value: String) -> config::Value {
56    if let Ok(value) = value.parse::<bool>() {
57        return value.into();
58    }
59    if let Ok(value) = value.parse::<i64>() {
60        return value.into();
61    }
62    if let Ok(value) = value.parse::<f64>() {
63        return value.into();
64    }
65    value.into()
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn normalize_env_key_handles_prefixes_and_nested_keys() {
74        assert_eq!(
75            normalize_env_key("", "SERVICE__PORT").as_deref(),
76            Some("service.port")
77        );
78        assert_eq!(
79            normalize_env_key("APP", "APP__SERVICE__DEBUG").as_deref(),
80            Some("service.debug")
81        );
82        assert_eq!(normalize_env_key("APP", "OTHER__VALUE"), None);
83    }
84
85    #[test]
86    fn parse_env_value_preserves_basic_types() {
87        assert!(parse_env_value("true".to_string()).into_bool().unwrap());
88        assert_eq!(parse_env_value("42".to_string()).into_int().unwrap(), 42);
89        assert_eq!(
90            parse_env_value("1.5".to_string()).into_float().unwrap(),
91            1.5
92        );
93        assert_eq!(
94            parse_env_value("plain".to_string()).into_string().unwrap(),
95            "plain"
96        );
97    }
98}