ricecoder_storage/config/
env.rs

1//! Environment variable override support
2//!
3//! This module provides parsing and application of environment variables with
4//! the RICECODER_ prefix to override configuration values.
5
6use super::Config;
7use std::collections::HashMap;
8
9/// Environment variable overrides
10pub struct EnvOverrides;
11
12impl EnvOverrides {
13    /// Parse environment variables with RICECODER_ prefix
14    ///
15    /// Returns a map of configuration paths to values.
16    /// For example, RICECODER_PROVIDERS_DEFAULT=openai becomes
17    /// {"providers.default_provider": "openai"}
18    pub fn parse() -> HashMap<String, String> {
19        let mut overrides = HashMap::new();
20
21        for (key, value) in std::env::vars() {
22            if let Some(config_key) = key.strip_prefix("RICECODER_") {
23                let config_path = Self::env_key_to_config_path(config_key);
24                overrides.insert(config_path, value);
25            }
26        }
27
28        overrides
29    }
30
31    /// Apply environment variable overrides to configuration
32    ///
33    /// This function applies environment variable overrides to the configuration
34    /// by parsing the environment and updating the config accordingly.
35    pub fn apply(config: &mut Config) {
36        let overrides = Self::parse();
37        Self::apply_overrides(config, &overrides);
38    }
39
40    /// Apply specific overrides to configuration
41    pub fn apply_overrides(config: &mut Config, overrides: &HashMap<String, String>) {
42        for (path, value) in overrides {
43            Self::set_config_value(config, path, value);
44        }
45    }
46
47    /// Convert environment variable key to configuration path
48    ///
49    /// Examples:
50    /// - PROVIDERS_DEFAULT -> providers.default_provider
51    /// - PROVIDERS_API_KEYS_OPENAI -> providers.api_keys.openai
52    /// - DEFAULTS_MODEL -> defaults.model
53    fn env_key_to_config_path(key: &str) -> String {
54        key.to_lowercase().replace('_', ".")
55    }
56
57    /// Set a configuration value by path
58    ///
59    /// Supports nested paths like "providers.default_provider"
60    fn set_config_value(config: &mut Config, path: &str, value: &str) {
61        let parts: Vec<&str> = path.split('.').collect();
62
63        match parts.as_slice() {
64            ["providers", "default_provider"] => {
65                config.providers.default_provider = Some(value.to_string());
66            }
67            ["providers", "api_keys", key] => {
68                config
69                    .providers
70                    .api_keys
71                    .insert(key.to_string(), value.to_string());
72            }
73            ["providers", "endpoints", key] => {
74                config
75                    .providers
76                    .endpoints
77                    .insert(key.to_string(), value.to_string());
78            }
79            ["defaults", "model"] => {
80                config.defaults.model = Some(value.to_string());
81            }
82            ["defaults", "temperature"] => {
83                if let Ok(temp) = value.parse::<f32>() {
84                    config.defaults.temperature = Some(temp);
85                }
86            }
87            ["defaults", "max_tokens"] => {
88                if let Ok(tokens) = value.parse::<u32>() {
89                    config.defaults.max_tokens = Some(tokens);
90                }
91            }
92            _ => {
93                // Store in custom map for unknown paths
94                if let Ok(json_value) = serde_json::from_str(value) {
95                    config.custom.insert(path.to_string(), json_value);
96                } else {
97                    config.custom.insert(
98                        path.to_string(),
99                        serde_json::Value::String(value.to_string()),
100                    );
101                }
102            }
103        }
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_env_key_to_config_path() {
113        assert_eq!(
114            EnvOverrides::env_key_to_config_path("PROVIDERS_DEFAULT"),
115            "providers.default"
116        );
117        assert_eq!(
118            EnvOverrides::env_key_to_config_path("DEFAULTS_MODEL"),
119            "defaults.model"
120        );
121    }
122
123    #[test]
124    fn test_apply_provider_default_override() {
125        let mut config = Config::default();
126        let mut overrides = HashMap::new();
127        overrides.insert(
128            "providers.default_provider".to_string(),
129            "openai".to_string(),
130        );
131
132        EnvOverrides::apply_overrides(&mut config, &overrides);
133
134        assert_eq!(
135            config.providers.default_provider,
136            Some("openai".to_string())
137        );
138    }
139
140    #[test]
141    fn test_apply_api_key_override() {
142        let mut config = Config::default();
143        let mut overrides = HashMap::new();
144        overrides.insert(
145            "providers.api_keys.openai".to_string(),
146            "test-key".to_string(),
147        );
148
149        EnvOverrides::apply_overrides(&mut config, &overrides);
150
151        assert_eq!(
152            config.providers.api_keys.get("openai"),
153            Some(&"test-key".to_string())
154        );
155    }
156
157    #[test]
158    fn test_apply_defaults_override() {
159        let mut config = Config::default();
160        let mut overrides = HashMap::new();
161        overrides.insert("defaults.model".to_string(), "gpt-4".to_string());
162        overrides.insert("defaults.temperature".to_string(), "0.7".to_string());
163        overrides.insert("defaults.max_tokens".to_string(), "2000".to_string());
164
165        EnvOverrides::apply_overrides(&mut config, &overrides);
166
167        assert_eq!(config.defaults.model, Some("gpt-4".to_string()));
168        assert_eq!(config.defaults.temperature, Some(0.7));
169        assert_eq!(config.defaults.max_tokens, Some(2000));
170    }
171
172    #[test]
173    fn test_apply_multiple_overrides() {
174        let mut config = Config::default();
175        let mut overrides = HashMap::new();
176        overrides.insert(
177            "providers.default_provider".to_string(),
178            "openai".to_string(),
179        );
180        overrides.insert("defaults.model".to_string(), "gpt-4".to_string());
181        overrides.insert(
182            "providers.api_keys.openai".to_string(),
183            "test-key".to_string(),
184        );
185
186        EnvOverrides::apply_overrides(&mut config, &overrides);
187
188        assert_eq!(
189            config.providers.default_provider,
190            Some("openai".to_string())
191        );
192        assert_eq!(config.defaults.model, Some("gpt-4".to_string()));
193        assert_eq!(
194            config.providers.api_keys.get("openai"),
195            Some(&"test-key".to_string())
196        );
197    }
198
199    #[test]
200    fn test_apply_custom_override() {
201        let mut config = Config::default();
202        let mut overrides = HashMap::new();
203        overrides.insert("custom.setting".to_string(), "value".to_string());
204
205        EnvOverrides::apply_overrides(&mut config, &overrides);
206
207        assert_eq!(
208            config.custom.get("custom.setting"),
209            Some(&serde_json::Value::String("value".to_string()))
210        );
211    }
212}