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, Default, Deserialize)]
16pub struct Settings {
17    /// Environment variable overrides.
18    #[serde(default)]
19    pub env: HashMap<String, String>,
20}
21
22/// An [`EnvSource`](crate::utils::env::EnvSource) that reads the real process
23/// environment first and falls back to the `env` map in
24/// `$HOME/.omni-dev/settings.json` — the value form of [`get_env_var`].
25///
26/// Pass `&SettingsEnv::load()` from a thin production wrapper; tests inject a
27/// pure `MapEnv` into the same `*_with(&impl EnvSource, …)` seam instead of
28/// mutating the process environment.
29#[derive(Debug, Default)]
30pub struct SettingsEnv {
31    settings: Settings,
32}
33
34impl SettingsEnv {
35    /// Loads settings from the default location, falling back to an empty
36    /// settings map if they are absent or unreadable (env-only behaviour).
37    pub fn load() -> Self {
38        Self {
39            settings: Settings::load().unwrap_or_default(),
40        }
41    }
42}
43
44impl crate::utils::env::EnvSource for SettingsEnv {
45    fn var(&self, key: &str) -> Option<String> {
46        env::var(key)
47            .ok()
48            .or_else(|| self.settings.env.get(key).cloned())
49    }
50}
51
52impl Settings {
53    /// Loads settings from the default location.
54    pub fn load() -> Result<Self> {
55        let settings_path = Self::get_settings_path()?;
56        Self::load_from_path(&settings_path)
57    }
58
59    /// Loads settings from a specific path.
60    pub fn load_from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
61        let path = path.as_ref();
62
63        // If file doesn't exist, return default settings
64        if !path.exists() {
65            return Ok(Self {
66                env: HashMap::new(),
67            });
68        }
69
70        // Read and parse the settings file
71        let content = fs::read_to_string(path)
72            .with_context(|| format!("Failed to read settings file: {}", path.display()))?;
73
74        serde_json::from_str::<Self>(&content)
75            .with_context(|| format!("Failed to parse settings file: {}", path.display()))
76    }
77
78    /// Returns the default settings path.
79    pub fn get_settings_path() -> Result<PathBuf> {
80        let home_dir = dirs::home_dir().context("Failed to determine home directory")?;
81
82        Ok(home_dir.join(".omni-dev").join("settings.json"))
83    }
84
85    /// Returns an environment variable with fallback to settings.
86    pub fn get_env_var(&self, key: &str) -> Option<String> {
87        // Try to get from actual environment first
88        match env::var(key) {
89            Ok(value) => Some(value),
90            Err(_) => {
91                // Fall back to settings
92                self.env.get(key).cloned()
93            }
94        }
95    }
96}
97
98/// Returns an environment variable with fallback to settings.
99pub fn get_env_var(key: &str) -> Result<String> {
100    // Try to get from actual environment first
101    match env::var(key) {
102        Ok(value) => Ok(value),
103        Err(_) => {
104            // Try to load settings and check there
105            match Settings::load() {
106                Ok(settings) => settings
107                    .env
108                    .get(key)
109                    .cloned()
110                    .ok_or_else(|| anyhow::anyhow!("Environment variable not found: {key}")),
111                Err(err) => {
112                    // If we couldn't load settings, just return the original env var error
113                    Err(anyhow::anyhow!("Environment variable not found: {key}").context(err))
114                }
115            }
116        }
117    }
118}
119
120/// Tries multiple environment variables with fallback to settings.
121pub fn get_env_vars(keys: &[&str]) -> Result<String> {
122    for key in keys {
123        if let Ok(value) = get_env_var(key) {
124            return Ok(value);
125        }
126    }
127
128    Err(anyhow::anyhow!(
129        "None of the environment variables found: {keys:?}"
130    ))
131}
132
133#[cfg(test)]
134#[allow(clippy::unwrap_used, clippy::expect_used)]
135mod tests {
136    use super::*;
137    use std::fs;
138    use tempfile::TempDir;
139
140    #[test]
141    fn settings_load_from_path() {
142        // Create a temporary directory (use current dir to avoid TMPDIR issues in tarpaulin)
143        let temp_dir = {
144            std::fs::create_dir_all("tmp").ok();
145            TempDir::new_in("tmp").unwrap()
146        };
147        let settings_path = temp_dir.path().join("settings.json");
148
149        // Create a test settings file
150        let settings_json = r#"{
151            "env": {
152                "TEST_VAR": "test_value",
153                "CLAUDE_API_KEY": "test_api_key"
154            }
155        }"#;
156        fs::write(&settings_path, settings_json).unwrap();
157
158        // Load settings
159        let settings = Settings::load_from_path(&settings_path).unwrap();
160
161        // Check env vars
162        assert_eq!(settings.env.get("TEST_VAR").unwrap(), "test_value");
163        assert_eq!(settings.env.get("CLAUDE_API_KEY").unwrap(), "test_api_key");
164    }
165
166    #[test]
167    fn settings_get_env_var() {
168        // Create a temporary directory (use current dir to avoid TMPDIR issues in tarpaulin)
169        let temp_dir = {
170            std::fs::create_dir_all("tmp").ok();
171            TempDir::new_in("tmp").unwrap()
172        };
173        let settings_path = temp_dir.path().join("settings.json");
174
175        // Create a test settings file
176        let settings_json = r#"{
177            "env": {
178                "TEST_VAR": "test_value",
179                "CLAUDE_API_KEY": "test_api_key"
180            }
181        }"#;
182        fs::write(&settings_path, settings_json).unwrap();
183
184        // Load settings
185        let settings = Settings::load_from_path(&settings_path).unwrap();
186
187        // Set actual environment variable
188        env::set_var("TEST_VAR_ENV", "env_value");
189
190        // Test precedence - env var should take precedence
191        env::set_var("TEST_VAR", "env_override");
192        assert_eq!(settings.get_env_var("TEST_VAR").unwrap(), "env_override");
193
194        // Test fallback to settings
195        env::remove_var("TEST_VAR"); // Remove from environment
196        assert_eq!(settings.get_env_var("TEST_VAR").unwrap(), "test_value");
197
198        // Test actual env var
199        assert_eq!(settings.get_env_var("TEST_VAR_ENV").unwrap(), "env_value");
200
201        // Clean up
202        env::remove_var("TEST_VAR_ENV");
203    }
204}