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