foxy/config/
env.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Environment variable-based configuration provider implementation.
6
7use std::collections::HashMap;
8use std::env;
9use serde_json::{Value, json};
10
11use super::ConfigProvider;
12use super::ConfigError;
13
14/// Configuration provider that retrieves values from environment variables.
15#[derive(Debug)]
16pub struct EnvConfigProvider {
17    /// Prefix for environment variables (e.g., "FOXY_").
18    prefix: String,
19    /// Cache of environment variables that match the prefix.
20    cache: HashMap<String, String>,
21}
22
23impl EnvConfigProvider {
24    /// Create a new environment variable configuration provider with the specified prefix.
25    pub fn new(prefix: &str) -> Self {
26        let mut provider = Self {
27            prefix: prefix.to_string(),
28            cache: HashMap::new(),
29        };
30
31        // Pre-load all environment variables with the specified prefix
32        provider.refresh_cache();
33
34        provider
35    }
36
37    /// Create a new environment variable configuration provider with the default "FOXY_" prefix.
38    pub fn default() -> Self {
39        Self::new("FOXY_")
40    }
41
42    /// Refresh the cache of environment variables.
43    pub fn refresh_cache(&mut self) {
44        self.cache.clear();
45
46        for (key, value) in env::vars() {
47            if key.starts_with(&self.prefix) {
48                // Strip the prefix and convert to lowercase for consistent key lookup
49                let config_key = key[self.prefix.len()..].to_lowercase();
50                // Convert underscores to dots for nested keys (e.g., FOXY_SERVER_HOST -> server.host)
51                let config_key = config_key.replace('_', ".");
52
53                self.cache.insert(config_key, value);
54            }
55        }
56    }
57
58    /// Parse a string value into a JSON Value.
59    fn parse_value_to_json(&self, value: &str) -> Result<Value, ConfigError> {
60        // Try to parse as JSON first
61        if let Ok(json_value) = serde_json::from_str(value) {
62            return Ok(json_value);
63        }
64
65        // If JSON parsing fails, try to determine the type and convert
66
67        // Try boolean
68        if value.eq_ignore_ascii_case("true") {
69            return Ok(json!(true));
70        } else if value.eq_ignore_ascii_case("false") {
71            return Ok(json!(false));
72        }
73
74        // Try number
75        if let Ok(int_val) = value.parse::<i64>() {
76            return Ok(json!(int_val));
77        }
78
79        if let Ok(float_val) = value.parse::<f64>() {
80            return Ok(json!(float_val));
81        }
82
83        // Default to string
84        Ok(json!(value))
85    }
86}
87
88impl ConfigProvider for EnvConfigProvider {
89    fn get_raw(&self, key: &str) -> Result<Option<Value>, ConfigError> {
90        match self.cache.get(key) {
91            Some(value) => self.parse_value_to_json(value).map(Some),
92            None => Ok(None),
93        }
94    }
95
96    fn has(&self, key: &str) -> bool {
97        self.cache.contains_key(key)
98    }
99
100    fn provider_name(&self) -> &str {
101        "env"
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use std::env;
109    use crate::config::ConfigProviderExt;
110
111    #[test]
112    fn test_env_provider() {
113        // Set some test environment variables
114        unsafe {
115            env::set_var("FOXY_SERVER_HOST", "localhost");
116            env::set_var("FOXY_SERVER_PORT", "9090");
117            env::set_var("FOXY_DEBUG", "true");
118        }
119
120        let provider = EnvConfigProvider::default();
121
122        assert_eq!(provider.has("server.host"), true);
123        assert_eq!(provider.has("nonexistent"), false);
124
125        let host: String = provider.get("server.host").unwrap().unwrap();
126        assert_eq!(host, "localhost");
127
128        let port: u16 = provider.get("server.port").unwrap().unwrap();
129        assert_eq!(port, 9090);
130
131        let debug: bool = provider.get("debug").unwrap().unwrap();
132        assert_eq!(debug, true);
133
134        // Clean up
135        unsafe {
136            env::remove_var("FOXY_SERVER_HOST");
137            env::remove_var("FOXY_SERVER_PORT");
138            env::remove_var("FOXY_DEBUG");
139        }
140    }
141
142    #[test]
143    fn test_custom_prefix() {
144        unsafe {
145            env::set_var("CUSTOM_HOST", "customhost");
146        }
147
148        let provider = EnvConfigProvider::new("CUSTOM_");
149
150        assert_eq!(provider.has("host"), true);
151        let host: String = provider.get("host").unwrap().unwrap();
152        assert_eq!(host, "customhost");
153
154        // Clean up
155        unsafe {
156            env::remove_var("CUSTOM_HOST");
157        }
158    }
159
160    #[test]
161    fn test_cache_refresh() {
162        let mut provider = EnvConfigProvider::new("TEST_");
163
164        // Initially there should be no values
165        assert_eq!(provider.has("value"), false);
166
167        // Set a value after initialization
168        unsafe {
169            env::set_var("TEST_VALUE", "42");
170        }
171
172        // Should still be false as the cache hasn't been refreshed
173        assert_eq!(provider.has("value"), false);
174
175        // Refresh the cache
176        provider.refresh_cache();
177
178        // Now it should be available
179        assert_eq!(provider.has("value"), true);
180        let value: i32 = provider.get("value").unwrap().unwrap();
181        assert_eq!(value, 42);
182
183        // Clean up
184        unsafe {
185            env::remove_var("TEST_VALUE");
186        }
187    }
188}