Skip to main content

rez_next_common/
config.rs

1//! Configuration management for rez-core
2
3use serde::{Deserialize, Serialize};
4use std::env;
5use std::path::PathBuf;
6
7/// Configuration for rez-core components
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct RezCoreConfig {
10    /// Enable Rust version system
11    pub use_rust_version: bool,
12
13    /// Enable Rust solver
14    pub use_rust_solver: bool,
15
16    /// Enable Rust repository system
17    pub use_rust_repository: bool,
18
19    /// Fallback to Python on Rust errors
20    pub rust_fallback: bool,
21
22    /// Number of threads for parallel operations
23    pub thread_count: Option<usize>,
24
25    /// Cache configuration
26    pub cache: CacheConfig,
27
28    /// Package search paths
29    pub packages_path: Vec<String>,
30
31    /// Local packages path
32    pub local_packages_path: String,
33
34    /// Release packages path
35    pub release_packages_path: String,
36
37    /// Default shell
38    pub default_shell: String,
39
40    /// Rez version
41    pub version: String,
42
43    /// Plugin paths
44    pub plugin_path: Vec<String>,
45
46    /// Package cache paths
47    pub package_cache_path: Vec<String>,
48
49    /// Temporary directory
50    pub tmpdir: String,
51
52    /// Editor command
53    pub editor: String,
54
55    /// Image viewer command
56    pub image_viewer: String,
57
58    /// Browser command
59    pub browser: String,
60
61    /// Diff program
62    pub difftool: String,
63
64    /// Terminal type
65    pub terminal_emulator_command: String,
66}
67
68/// Cache configuration
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct CacheConfig {
71    /// Enable memory cache
72    pub enable_memory_cache: bool,
73
74    /// Enable disk cache
75    pub enable_disk_cache: bool,
76
77    /// Memory cache size (number of entries)
78    pub memory_cache_size: usize,
79
80    /// Cache TTL in seconds
81    pub cache_ttl_seconds: u64,
82}
83
84impl RezCoreConfig {
85    pub fn new() -> Self {
86        Self::default()
87    }
88}
89
90impl Default for RezCoreConfig {
91    fn default() -> Self {
92        Self {
93            use_rust_version: true,
94            use_rust_solver: true,
95            use_rust_repository: true,
96            rust_fallback: true,
97            thread_count: None, // Use system default
98            cache: CacheConfig::default(),
99            packages_path: vec![
100                "~/packages".to_string(),
101                "~/.rez/packages/int".to_string(),
102                "~/.rez/packages/ext".to_string(),
103            ],
104            local_packages_path: "~/packages".to_string(),
105            release_packages_path: "~/.rez/packages/int".to_string(),
106            default_shell: if cfg!(windows) { "cmd" } else { "bash" }.to_string(),
107            version: env!("CARGO_PKG_VERSION").to_string(),
108            plugin_path: vec![],
109            package_cache_path: vec!["~/.rez/cache".to_string()],
110            tmpdir: std::env::temp_dir().to_string_lossy().to_string(),
111            editor: if cfg!(windows) { "notepad" } else { "vi" }.to_string(),
112            image_viewer: if cfg!(windows) { "mspaint" } else { "xdg-open" }.to_string(),
113            browser: if cfg!(windows) { "start" } else { "xdg-open" }.to_string(),
114            difftool: if cfg!(windows) { "fc" } else { "diff" }.to_string(),
115            terminal_emulator_command: if cfg!(windows) {
116                "cmd /c start cmd"
117            } else {
118                "xterm"
119            }
120            .to_string(),
121        }
122    }
123}
124
125impl Default for CacheConfig {
126    fn default() -> Self {
127        Self {
128            enable_memory_cache: true,
129            enable_disk_cache: true,
130            memory_cache_size: 1000,
131            cache_ttl_seconds: 3600, // 1 hour
132        }
133    }
134}
135
136impl RezCoreConfig {
137    /// Get the list of configuration file paths that are searched
138    pub fn get_search_paths() -> Vec<PathBuf> {
139        let mut paths = Vec::new();
140
141        // 1. Built-in config (if exists)
142        if let Ok(exe_path) = env::current_exe() {
143            if let Some(exe_dir) = exe_path.parent() {
144                paths.push(exe_dir.join("rezconfig.yaml"));
145                paths.push(exe_dir.join("rezconfig.json"));
146            }
147        }
148
149        // 2. Environment variable REZ_CONFIG_FILE
150        if let Ok(config_file) = env::var("REZ_CONFIG_FILE") {
151            for path in config_file.split(std::path::MAIN_SEPARATOR) {
152                paths.push(PathBuf::from(path));
153            }
154        }
155
156        // 3. System-wide config
157        if cfg!(unix) {
158            paths.push(PathBuf::from("/etc/rez/config.yaml"));
159            paths.push(PathBuf::from("/usr/local/etc/rez/config.yaml"));
160        } else if cfg!(windows) {
161            if let Ok(program_data) = env::var("PROGRAMDATA") {
162                paths.push(PathBuf::from(program_data).join("rez").join("config.yaml"));
163            }
164        }
165
166        // 4. User home config (unless disabled)
167        if env::var("REZ_DISABLE_HOME_CONFIG")
168            .unwrap_or_default()
169            .to_lowercase()
170            != "1"
171        {
172            if let Ok(home) = env::var("HOME") {
173                let home_path = PathBuf::from(&home);
174                paths.push(home_path.join(".rezconfig"));
175                paths.push(home_path.join(".rezconfig.yaml"));
176                paths.push(home_path.join(".rez").join("config.yaml"));
177            } else if cfg!(windows) {
178                if let Ok(userprofile) = env::var("USERPROFILE") {
179                    let user_path = PathBuf::from(&userprofile);
180                    paths.push(user_path.join(".rezconfig"));
181                    paths.push(user_path.join(".rezconfig.yaml"));
182                    paths.push(user_path.join(".rez").join("config.yaml"));
183                }
184            }
185        }
186
187        paths
188    }
189
190    /// Get the list of configuration files that actually exist and are sourced
191    pub fn get_sourced_paths() -> Vec<PathBuf> {
192        Self::get_search_paths()
193            .into_iter()
194            .filter(|path| path.exists())
195            .collect()
196    }
197
198    /// Load configuration from files (reads actual rezconfig files)
199    pub fn load() -> Self {
200        let mut config = Self::default();
201
202        // Try to load from config files in priority order
203        for path in Self::get_search_paths() {
204            if path.exists() {
205                if let Ok(content) = std::fs::read_to_string(&path) {
206                    // Try YAML format
207                    if let Ok(loaded) = serde_yaml::from_str::<RezCoreConfig>(&content) {
208                        config = loaded;
209                        break;
210                    }
211                    // Try JSON format
212                    if let Ok(loaded) = serde_json::from_str::<RezCoreConfig>(&content) {
213                        config = loaded;
214                        break;
215                    }
216                }
217            }
218        }
219
220        // Override with environment variables
221        if let Ok(packages_path) = env::var("REZ_PACKAGES_PATH") {
222            config.packages_path = packages_path.split(':').map(|s| s.to_string()).collect();
223        }
224        if let Ok(local_path) = env::var("REZ_LOCAL_PACKAGES_PATH") {
225            config.local_packages_path = local_path;
226        }
227        if let Ok(release_path) = env::var("REZ_RELEASE_PACKAGES_PATH") {
228            config.release_packages_path = release_path;
229        }
230
231        config
232    }
233
234    /// Get a configuration field by dot-separated path
235    pub fn get_field(&self, field_path: &str) -> Option<serde_json::Value> {
236        let parts: Vec<&str> = field_path.split('.').collect();
237
238        // Convert config to JSON for easy field access
239        let config_json = serde_json::to_value(self).ok()?;
240
241        let mut current = &config_json;
242        for part in parts {
243            current = current.get(part)?;
244        }
245
246        Some(current.clone())
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn test_default_config_has_sensible_values() {
256        let cfg = RezCoreConfig::default();
257        assert!(cfg.use_rust_solver);
258        assert!(cfg.use_rust_version);
259        assert!(cfg.use_rust_repository);
260        assert!(cfg.rust_fallback);
261        assert!(!cfg.packages_path.is_empty());
262        assert!(!cfg.local_packages_path.is_empty());
263        assert!(!cfg.release_packages_path.is_empty());
264        assert!(!cfg.version.is_empty());
265    }
266
267    #[test]
268    fn test_default_cache_config() {
269        let cfg = RezCoreConfig::default();
270        assert!(cfg.cache.enable_memory_cache);
271        assert!(cfg.cache.enable_disk_cache);
272        assert!(cfg.cache.memory_cache_size > 0);
273        assert!(cfg.cache.cache_ttl_seconds > 0);
274    }
275
276    #[test]
277    fn test_get_field_simple() {
278        let cfg = RezCoreConfig::default();
279        let v = cfg.get_field("version");
280        assert!(v.is_some());
281        if let Some(serde_json::Value::String(s)) = v {
282            assert!(!s.is_empty());
283        }
284    }
285
286    #[test]
287    fn test_get_field_packages_path() {
288        let cfg = RezCoreConfig::default();
289        let v = cfg.get_field("packages_path");
290        assert!(v.is_some());
291        if let Some(serde_json::Value::Array(arr)) = v {
292            assert!(!arr.is_empty());
293        }
294    }
295
296    #[test]
297    fn test_get_field_nested() {
298        let cfg = RezCoreConfig::default();
299        let v = cfg.get_field("cache.enable_memory_cache");
300        assert!(v.is_some());
301        assert_eq!(v, Some(serde_json::Value::Bool(true)));
302    }
303
304    #[test]
305    fn test_get_field_nested_numeric() {
306        let cfg = RezCoreConfig::default();
307        let v = cfg.get_field("cache.memory_cache_size");
308        assert!(v.is_some());
309    }
310
311    #[test]
312    fn test_get_field_nonexistent() {
313        let cfg = RezCoreConfig::default();
314        assert!(cfg.get_field("nonexistent_field").is_none());
315        assert!(cfg.get_field("cache.nonexistent").is_none());
316    }
317
318    #[test]
319    fn test_get_search_paths_not_empty() {
320        let paths = RezCoreConfig::get_search_paths();
321        assert!(!paths.is_empty());
322    }
323
324    #[test]
325    fn test_get_search_paths_contain_home_config() {
326        let paths = RezCoreConfig::get_search_paths();
327        let has_home = paths.iter().any(|p| {
328            p.to_string_lossy().contains(".rezconfig") || p.to_string_lossy().contains(".rez")
329        });
330        assert!(has_home);
331    }
332
333    #[test]
334    fn test_load_returns_config() {
335        // Should not panic, even if no config file exists
336        let cfg = RezCoreConfig::load();
337        assert!(!cfg.version.is_empty());
338    }
339
340    #[test]
341    fn test_env_override_packages_path() {
342        // Only safe to test if env var is not already set
343        if std::env::var("REZ_PACKAGES_PATH").is_err() {
344            std::env::set_var("REZ_PACKAGES_PATH", "/tmp/test_pkgs:/tmp/other_pkgs");
345            let cfg = RezCoreConfig::load();
346            assert!(cfg.packages_path.contains(&"/tmp/test_pkgs".to_string()));
347            std::env::remove_var("REZ_PACKAGES_PATH");
348        }
349    }
350
351    #[test]
352    fn test_config_serialization_roundtrip() {
353        let cfg = RezCoreConfig::default();
354        let json = serde_json::to_string(&cfg).unwrap();
355        let restored: RezCoreConfig = serde_json::from_str(&json).unwrap();
356        assert_eq!(cfg.version, restored.version);
357        assert_eq!(cfg.packages_path, restored.packages_path);
358        assert_eq!(
359            cfg.cache.memory_cache_size,
360            restored.cache.memory_cache_size
361        );
362    }
363
364    #[test]
365    fn test_config_clone() {
366        let cfg = RezCoreConfig::default();
367        let cloned = cfg.clone();
368        assert_eq!(cfg.version, cloned.version);
369        assert_eq!(cfg.packages_path, cloned.packages_path);
370    }
371}