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(Self {
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::<Self>(&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        if let Ok(value) = get_env_var(key) {
94            return Ok(value);
95        }
96    }
97
98    Err(anyhow::anyhow!(
99        "None of the environment variables found: {keys:?}"
100    ))
101}
102
103#[cfg(test)]
104#[allow(clippy::unwrap_used, clippy::expect_used)]
105mod tests {
106    use super::*;
107    use std::fs;
108    use tempfile::TempDir;
109
110    #[test]
111    fn settings_load_from_path() {
112        // Create a temporary directory (use current dir to avoid TMPDIR issues in tarpaulin)
113        let temp_dir = {
114            std::fs::create_dir_all("tmp").ok();
115            TempDir::new_in("tmp").unwrap()
116        };
117        let settings_path = temp_dir.path().join("settings.json");
118
119        // Create a test settings file
120        let settings_json = r#"{
121            "env": {
122                "TEST_VAR": "test_value",
123                "CLAUDE_API_KEY": "test_api_key"
124            }
125        }"#;
126        fs::write(&settings_path, settings_json).unwrap();
127
128        // Load settings
129        let settings = Settings::load_from_path(&settings_path).unwrap();
130
131        // Check env vars
132        assert_eq!(settings.env.get("TEST_VAR").unwrap(), "test_value");
133        assert_eq!(settings.env.get("CLAUDE_API_KEY").unwrap(), "test_api_key");
134    }
135
136    #[test]
137    fn settings_get_env_var() {
138        // Create a temporary directory (use current dir to avoid TMPDIR issues in tarpaulin)
139        let temp_dir = {
140            std::fs::create_dir_all("tmp").ok();
141            TempDir::new_in("tmp").unwrap()
142        };
143        let settings_path = temp_dir.path().join("settings.json");
144
145        // Create a test settings file
146        let settings_json = r#"{
147            "env": {
148                "TEST_VAR": "test_value",
149                "CLAUDE_API_KEY": "test_api_key"
150            }
151        }"#;
152        fs::write(&settings_path, settings_json).unwrap();
153
154        // Load settings
155        let settings = Settings::load_from_path(&settings_path).unwrap();
156
157        // Set actual environment variable
158        env::set_var("TEST_VAR_ENV", "env_value");
159
160        // Test precedence - env var should take precedence
161        env::set_var("TEST_VAR", "env_override");
162        assert_eq!(settings.get_env_var("TEST_VAR").unwrap(), "env_override");
163
164        // Test fallback to settings
165        env::remove_var("TEST_VAR"); // Remove from environment
166        assert_eq!(settings.get_env_var("TEST_VAR").unwrap(), "test_value");
167
168        // Test actual env var
169        assert_eq!(settings.get_env_var("TEST_VAR_ENV").unwrap(), "env_value");
170
171        // Clean up
172        env::remove_var("TEST_VAR_ENV");
173    }
174}