use serde::{Deserialize, Serialize};
use std::env;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(clippy::struct_excessive_bools)]
pub struct RezCoreConfig {
pub use_rust_version: bool,
pub use_rust_solver: bool,
pub use_rust_repository: bool,
pub rust_fallback: bool,
pub thread_count: Option<usize>,
pub cache: CacheConfig,
pub packages_path: Vec<String>,
pub local_packages_path: String,
pub release_packages_path: String,
pub default_shell: String,
pub version: String,
pub plugin_path: Vec<String>,
pub package_cache_path: Vec<String>,
pub tmpdir: String,
pub editor: String,
pub image_viewer: String,
pub browser: String,
pub difftool: String,
pub terminal_emulator_command: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheConfig {
pub enable_memory_cache: bool,
pub enable_disk_cache: bool,
pub memory_cache_size: usize,
pub cache_ttl_seconds: u64,
}
impl RezCoreConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
impl Default for RezCoreConfig {
fn default() -> Self {
Self {
use_rust_version: true,
use_rust_solver: true,
use_rust_repository: true,
rust_fallback: true,
thread_count: None, cache: CacheConfig::default(),
packages_path: vec![
"~/packages".to_string(),
"~/.rez/packages/int".to_string(),
"~/.rez/packages/ext".to_string(),
],
local_packages_path: "~/packages".to_string(),
release_packages_path: "~/.rez/packages/int".to_string(),
default_shell: if cfg!(windows) { "cmd" } else { "bash" }.to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
plugin_path: vec![],
package_cache_path: vec!["~/.rez/cache".to_string()],
tmpdir: std::env::temp_dir().to_string_lossy().to_string(),
editor: if cfg!(windows) { "notepad" } else { "vi" }.to_string(),
image_viewer: if cfg!(windows) { "mspaint" } else { "xdg-open" }.to_string(),
browser: if cfg!(windows) { "start" } else { "xdg-open" }.to_string(),
difftool: if cfg!(windows) { "fc" } else { "diff" }.to_string(),
terminal_emulator_command: if cfg!(windows) {
"cmd /c start cmd"
} else {
"xterm"
}
.to_string(),
}
}
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
enable_memory_cache: true,
enable_disk_cache: true,
memory_cache_size: 1000,
cache_ttl_seconds: 3600, }
}
}
impl RezCoreConfig {
#[must_use]
pub fn get_search_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
if let Ok(exe_path) = env::current_exe()
&& let Some(exe_dir) = exe_path.parent()
{
paths.push(exe_dir.join("rezconfig.yaml"));
paths.push(exe_dir.join("rezconfig.json"));
}
if let Ok(config_file) = env::var("REZ_CONFIG_FILE") {
for path in config_file.split(std::path::MAIN_SEPARATOR) {
paths.push(PathBuf::from(path));
}
}
if cfg!(unix) {
paths.push(PathBuf::from("/etc/rez/config.yaml"));
paths.push(PathBuf::from("/usr/local/etc/rez/config.yaml"));
} else if cfg!(windows)
&& let Ok(program_data) = env::var("PROGRAMDATA")
{
paths.push(PathBuf::from(program_data).join("rez").join("config.yaml"));
}
if env::var("REZ_DISABLE_HOME_CONFIG")
.unwrap_or_default()
.to_lowercase()
!= "1"
{
if let Ok(home) = env::var("HOME") {
let home_path = PathBuf::from(&home);
paths.push(home_path.join(".rezconfig"));
paths.push(home_path.join(".rezconfig.yaml"));
paths.push(home_path.join(".rez").join("config.yaml"));
} else if cfg!(windows)
&& let Ok(userprofile) = env::var("USERPROFILE")
{
let user_path = PathBuf::from(&userprofile);
paths.push(user_path.join(".rezconfig"));
paths.push(user_path.join(".rezconfig.yaml"));
paths.push(user_path.join(".rez").join("config.yaml"));
}
}
paths
}
#[must_use]
pub fn get_sourced_paths() -> Vec<PathBuf> {
Self::get_search_paths()
.into_iter()
.filter(|path| path.exists())
.collect()
}
#[must_use]
pub fn load() -> Self {
let mut config = Self::default();
for path in Self::get_search_paths() {
if path.exists()
&& let Ok(content) = std::fs::read_to_string(&path)
{
if let Ok(loaded) = serde_yaml::from_str::<RezCoreConfig>(&content) {
config = loaded;
break;
}
if let Ok(loaded) = serde_json::from_str::<RezCoreConfig>(&content) {
config = loaded;
break;
}
}
}
if let Ok(packages_path) = env::var("REZ_PACKAGES_PATH") {
config.packages_path = split_env_paths(&packages_path);
}
if let Ok(local_path) = env::var("REZ_LOCAL_PACKAGES_PATH") {
config.local_packages_path = local_path;
}
if let Ok(release_path) = env::var("REZ_RELEASE_PACKAGES_PATH") {
config.release_packages_path = release_path;
}
config
}
#[must_use]
pub fn get_field(&self, field_path: &str) -> Option<serde_json::Value> {
let parts: Vec<&str> = field_path.split('.').collect();
let config_json = serde_json::to_value(self).ok()?;
let mut current = &config_json;
for part in parts {
current = current.get(part)?;
}
Some(current.clone())
}
}
fn split_env_paths(paths: &str) -> Vec<String> {
let separator = if cfg!(windows) { ';' } else { ':' };
paths
.split(separator)
.map(str::trim)
.filter(|path| !path.is_empty())
.map(ToString::to_string)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config_has_sensible_values() {
let cfg = RezCoreConfig::default();
assert!(cfg.use_rust_solver);
assert!(cfg.use_rust_version);
assert!(cfg.use_rust_repository);
assert!(cfg.rust_fallback);
assert!(!cfg.packages_path.is_empty());
assert!(!cfg.local_packages_path.is_empty());
assert!(!cfg.release_packages_path.is_empty());
assert!(!cfg.version.is_empty());
}
#[test]
fn test_default_cache_config() {
let cfg = RezCoreConfig::default();
assert!(cfg.cache.enable_memory_cache);
assert!(cfg.cache.enable_disk_cache);
assert!(cfg.cache.memory_cache_size > 0);
assert!(cfg.cache.cache_ttl_seconds > 0);
}
#[test]
fn test_get_field_simple() {
let cfg = RezCoreConfig::default();
let v = cfg.get_field("version");
assert!(v.is_some());
if let Some(serde_json::Value::String(s)) = v {
assert!(!s.is_empty());
}
}
#[test]
fn test_get_field_packages_path() {
let cfg = RezCoreConfig::default();
let v = cfg.get_field("packages_path");
assert!(v.is_some());
if let Some(serde_json::Value::Array(arr)) = v {
assert!(!arr.is_empty());
}
}
#[test]
fn test_get_field_nested() {
let cfg = RezCoreConfig::default();
let v = cfg.get_field("cache.enable_memory_cache");
assert!(v.is_some());
assert_eq!(v, Some(serde_json::Value::Bool(true)));
}
#[test]
fn test_get_field_nested_numeric() {
let cfg = RezCoreConfig::default();
let v = cfg.get_field("cache.memory_cache_size");
assert!(v.is_some());
}
#[test]
fn test_get_field_nonexistent() {
let cfg = RezCoreConfig::default();
assert!(cfg.get_field("nonexistent_field").is_none());
assert!(cfg.get_field("cache.nonexistent").is_none());
}
#[test]
fn test_get_search_paths_not_empty() {
let paths = RezCoreConfig::get_search_paths();
assert!(!paths.is_empty());
}
#[test]
fn test_get_search_paths_contain_home_config() {
let paths = RezCoreConfig::get_search_paths();
let has_home = paths.iter().any(|p| {
p.to_string_lossy().contains(".rezconfig") || p.to_string_lossy().contains(".rez")
});
assert!(has_home);
}
#[test]
fn test_load_returns_config() {
let cfg = RezCoreConfig::load();
assert!(!cfg.version.is_empty());
}
#[test]
fn test_env_override_packages_path() {
if std::env::var("REZ_PACKAGES_PATH").is_err() {
unsafe {
let separator = if cfg!(windows) { ';' } else { ':' };
std::env::set_var(
"REZ_PACKAGES_PATH",
format!("/tmp/test_pkgs{separator}/tmp/other_pkgs"),
);
};
let cfg = RezCoreConfig::load();
assert!(cfg.packages_path.contains(&"/tmp/test_pkgs".to_string()));
unsafe {
std::env::remove_var("REZ_PACKAGES_PATH");
};
}
}
#[test]
fn test_split_env_paths_uses_platform_separator() {
let separator = if cfg!(windows) { ';' } else { ':' };
let result = split_env_paths(&format!("first{separator}second"));
assert_eq!(result, vec!["first".to_string(), "second".to_string()]);
}
#[cfg(windows)]
#[test]
fn test_split_env_paths_preserves_windows_drive_letters() {
let result = split_env_paths(r"C:\packages\rez;D:\local\packages");
assert_eq!(
result,
vec![
r"C:\packages\rez".to_string(),
r"D:\local\packages".to_string()
]
);
}
#[test]
fn test_config_serialization_roundtrip() {
let cfg = RezCoreConfig::default();
let json = serde_json::to_string(&cfg).unwrap();
let restored: RezCoreConfig = serde_json::from_str(&json).unwrap();
assert_eq!(cfg.version, restored.version);
assert_eq!(cfg.packages_path, restored.packages_path);
assert_eq!(
cfg.cache.memory_cache_size,
restored.cache.memory_cache_size
);
}
#[test]
fn test_config_clone() {
let cfg = RezCoreConfig::default();
let cloned = cfg.clone();
assert_eq!(cfg.version, cloned.version);
assert_eq!(cfg.packages_path, cloned.packages_path);
}
}