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