Skip to main content

omni_dev/utils/
settings.rs

1//! Settings and configuration utilities
2//!
3//! This module provides functionality to read settings from $HOME/.omni-dev/settings.json
4//! and use them as a fallback for environment variables.
5
6use anyhow::{Context, Result};
7use serde::Deserialize;
8use std::collections::HashMap;
9use std::env;
10use std::fs;
11use std::path::{Path, PathBuf};
12
13/// Settings loaded from $HOME/.omni-dev/settings.json
14#[derive(Debug, Deserialize)]
15pub struct Settings {
16    /// Environment variable overrides
17    #[serde(default)]
18    pub env: HashMap<String, String>,
19}
20
21impl Settings {
22    /// Load settings from the default location
23    pub fn load() -> Result<Self> {
24        let settings_path = Self::get_settings_path()?;
25        Self::load_from_path(&settings_path)
26    }
27
28    /// Load settings from a specific path
29    pub fn load_from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
30        let path = path.as_ref();
31
32        // If file doesn't exist, return default settings
33        if !path.exists() {
34            return Ok(Settings {
35                env: HashMap::new(),
36            });
37        }
38
39        // Read and parse the settings file
40        let content = fs::read_to_string(path)
41            .with_context(|| format!("Failed to read settings file: {}", path.display()))?;
42
43        serde_json::from_str::<Settings>(&content)
44            .with_context(|| format!("Failed to parse settings file: {}", path.display()))
45    }
46
47    /// Get the default settings path
48    pub fn get_settings_path() -> Result<PathBuf> {
49        let home_dir = dirs::home_dir().context("Failed to determine home directory")?;
50
51        Ok(home_dir.join(".omni-dev").join("settings.json"))
52    }
53
54    /// Get an environment variable with fallback to settings
55    pub fn get_env_var(&self, key: &str) -> Option<String> {
56        // Try to get from actual environment first
57        match env::var(key) {
58            Ok(value) => Some(value),
59            Err(_) => {
60                // Fall back to settings
61                self.env.get(key).cloned()
62            }
63        }
64    }
65}
66
67/// Get an environment variable with fallback to settings
68pub fn get_env_var(key: &str) -> Result<String> {
69    // Try to get from actual environment first
70    match env::var(key) {
71        Ok(value) => Ok(value),
72        Err(_) => {
73            // Try to load settings and check there
74            match Settings::load() {
75                Ok(settings) => settings
76                    .env
77                    .get(key)
78                    .cloned()
79                    .ok_or_else(|| anyhow::anyhow!("Environment variable not found: {}", key)),
80                Err(err) => {
81                    // If we couldn't load settings, just return the original env var error
82                    Err(anyhow::anyhow!("Environment variable not found: {}", key).context(err))
83                }
84            }
85        }
86    }
87}
88
89/// Try multiple environment variables with fallback to settings
90pub fn get_env_vars(keys: &[&str]) -> Result<String> {
91    for key in keys {
92        match get_env_var(key) {
93            Ok(value) => return Ok(value),
94            Err(_) => continue,
95        }
96    }
97
98    Err(anyhow::anyhow!(
99        "None of the environment variables found: {:?}",
100        keys
101    ))
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use std::fs;
108    use tempfile::TempDir;
109
110    #[test]
111    fn test_settings_load_from_path() {
112        // Create a temporary directory
113        let temp_dir = TempDir::new().unwrap();
114        let settings_path = temp_dir.path().join("settings.json");
115
116        // Create a test settings file
117        let settings_json = r#"{
118            "env": {
119                "TEST_VAR": "test_value",
120                "CLAUDE_API_KEY": "test_api_key"
121            }
122        }"#;
123        fs::write(&settings_path, settings_json).unwrap();
124
125        // Load settings
126        let settings = Settings::load_from_path(&settings_path).unwrap();
127
128        // Check env vars
129        assert_eq!(settings.env.get("TEST_VAR").unwrap(), "test_value");
130        assert_eq!(settings.env.get("CLAUDE_API_KEY").unwrap(), "test_api_key");
131    }
132
133    #[test]
134    fn test_settings_get_env_var() {
135        // Create a temporary directory
136        let temp_dir = TempDir::new().unwrap();
137        let settings_path = temp_dir.path().join("settings.json");
138
139        // Create a test settings file
140        let settings_json = r#"{
141            "env": {
142                "TEST_VAR": "test_value",
143                "CLAUDE_API_KEY": "test_api_key"
144            }
145        }"#;
146        fs::write(&settings_path, settings_json).unwrap();
147
148        // Load settings
149        let settings = Settings::load_from_path(&settings_path).unwrap();
150
151        // Set actual environment variable
152        env::set_var("TEST_VAR_ENV", "env_value");
153
154        // Test precedence - env var should take precedence
155        env::set_var("TEST_VAR", "env_override");
156        assert_eq!(settings.get_env_var("TEST_VAR").unwrap(), "env_override");
157
158        // Test fallback to settings
159        env::remove_var("TEST_VAR"); // Remove from environment
160        assert_eq!(settings.get_env_var("TEST_VAR").unwrap(), "test_value");
161
162        // Test actual env var
163        assert_eq!(settings.get_env_var("TEST_VAR_ENV").unwrap(), "env_value");
164
165        // Clean up
166        env::remove_var("TEST_VAR_ENV");
167    }
168}