rez_next_common/
config.rs1use serde::{Deserialize, Serialize};
4use std::env;
5use std::path::PathBuf;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9#[allow(clippy::struct_excessive_bools)]
10pub struct RezCoreConfig {
11 pub use_rust_version: bool,
13
14 pub use_rust_solver: bool,
16
17 pub use_rust_repository: bool,
19
20 pub rust_fallback: bool,
22
23 pub thread_count: Option<usize>,
25
26 pub cache: CacheConfig,
28
29 pub packages_path: Vec<String>,
31
32 pub local_packages_path: String,
34
35 pub release_packages_path: String,
37
38 pub default_shell: String,
40
41 pub version: String,
43
44 pub plugin_path: Vec<String>,
46
47 pub package_cache_path: Vec<String>,
49
50 pub tmpdir: String,
52
53 pub editor: String,
55
56 pub image_viewer: String,
58
59 pub browser: String,
61
62 pub difftool: String,
64
65 pub terminal_emulator_command: String,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct CacheConfig {
72 pub enable_memory_cache: bool,
74
75 pub enable_disk_cache: bool,
77
78 pub memory_cache_size: usize,
80
81 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, 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, }
135 }
136}
137
138impl RezCoreConfig {
139 #[must_use]
141 pub fn get_search_paths() -> Vec<PathBuf> {
142 let mut paths = Vec::new();
143
144 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 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 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 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 #[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 #[must_use]
204 pub fn load() -> Self {
205 let mut config = Self::default();
206
207 for path in Self::get_search_paths() {
209 if path.exists()
210 && let Ok(content) = std::fs::read_to_string(&path)
211 {
212 if let Ok(loaded) = serde_yaml::from_str::<RezCoreConfig>(&content) {
214 config = loaded;
215 break;
216 }
217 if let Ok(loaded) = serde_json::from_str::<RezCoreConfig>(&content) {
219 config = loaded;
220 break;
221 }
222 }
223 }
224
225 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 #[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 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 let cfg = RezCoreConfig::load();
353 assert!(!cfg.version.is_empty());
354 }
355
356 #[test]
357 fn test_env_override_packages_path() {
358 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}