Skip to main content

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 serde_json::{Value, json};
8use std::collections::HashMap;
9use std::env;
10
11use super::ConfigError;
12use super::ConfigProvider;
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    /// Refresh the cache of environment variables.
38    pub fn refresh_cache(&mut self) {
39        self.cache.clear();
40
41        for (key, value) in env::vars() {
42            if key.starts_with(&self.prefix) {
43                // Strip the prefix and convert to lowercase for consistent key lookup
44                let config_key = key[self.prefix.len()..].to_lowercase();
45                // Convert underscores to dots for nested keys (e.g., FOXY_SERVER_HOST -> server.host)
46                let config_key = config_key.replace('_', ".");
47
48                self.cache.insert(config_key, value);
49            }
50        }
51    }
52
53    /// Parse a string value into a JSON Value.
54    fn parse_value_to_json(&self, value: &str) -> Result<Value, ConfigError> {
55        // Try to parse as JSON first
56        if let Ok(json_value) = serde_json::from_str(value) {
57            return Ok(json_value);
58        }
59
60        // If JSON parsing fails, try to determine the type and convert
61
62        // Try boolean
63        if value.eq_ignore_ascii_case("true") {
64            return Ok(json!(true));
65        } else if value.eq_ignore_ascii_case("false") {
66            return Ok(json!(false));
67        }
68
69        // Try number
70        if let Ok(int_val) = value.parse::<i64>() {
71            return Ok(json!(int_val));
72        }
73
74        if let Ok(float_val) = value.parse::<f64>() {
75            return Ok(json!(float_val));
76        }
77
78        // Default to string
79        Ok(json!(value))
80    }
81}
82
83impl Default for EnvConfigProvider {
84    fn default() -> Self {
85        Self::new("FOXY_")
86    }
87}
88
89impl ConfigProvider for EnvConfigProvider {
90    fn get_raw(&self, key: &str) -> Result<Option<Value>, ConfigError> {
91        match self.cache.get(key) {
92            Some(value) => self.parse_value_to_json(value).map(Some),
93            None => Ok(None),
94        }
95    }
96
97    fn has(&self, key: &str) -> bool {
98        self.cache.contains_key(key)
99    }
100
101    fn provider_name(&self) -> &str {
102        "env"
103    }
104}