mod config_file;
mod home;
mod legacy;
mod project;
mod resolution;
mod types;
use std::path::PathBuf;
use thiserror::Error;
pub use config_file::{
app_config_to_value, flatten_app_config, read_app_config, read_tool_versions,
set_app_config_value, unset_app_config_value, write_app_config, write_tool_versions,
};
pub use home::{resolve_home, resolve_home_with};
pub use legacy::{
find_legacy_file, parse_legacy_versions, read_legacy_versions, supported_legacy_files,
};
pub use project::{find_project_file, preferred_project_file};
pub use resolution::{global_tools_file, resolve_tool_version, session_tools_file};
pub use types::{
AppConfig, CacheConfig, HomeLayout, LegacyVersionFileConfig, ProxyConfig, RegistryConfig,
ResolvedToolVersion, Scope, StorageConfig, ToolVersions,
};
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("failed to resolve the user home directory")]
HomeDirectoryUnavailable,
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("failed to parse TOML file at {path}: {message}")]
Toml { path: PathBuf, message: String },
#[error("failed to parse YAML file at {path}: {message}")]
Yaml { path: PathBuf, message: String },
#[error("unknown config key: {0}")]
UnknownKey(String),
#[error("invalid value {value} for key {key}")]
InvalidValue { key: String, value: String },
#[error("unsupported legacy file: {0}")]
UnsupportedLegacyFile(PathBuf),
}
#[cfg(test)]
mod tests {
use std::error::Error;
use std::fs;
use tempfile::TempDir;
use super::*;
#[test]
fn resolve_home_should_prefer_vs_home() -> Result<(), Box<dyn Error>> {
let temp_dir = TempDir::new()?;
let explicit_home = temp_dir.path().join("explicit");
let layout = resolve_home_with(Some(explicit_home.clone()), temp_dir.path().join("user"))?;
assert_eq!(layout.active_home, explicit_home);
assert!(layout.migration_candidates.is_empty());
Ok(())
}
#[test]
fn resolve_tool_version_should_prefer_project_config_over_global() -> Result<(), Box<dyn Error>>
{
let temp_dir = TempDir::new()?;
let home = temp_dir.path().join("home");
fs::create_dir_all(home.join("global"))?;
write_tool_versions(
&global_tools_file(&home),
&ToolVersions {
tools: [("nodejs".to_string(), "18.19.0".to_string())]
.into_iter()
.collect(),
},
)?;
let project = temp_dir.path().join("project");
fs::create_dir_all(&project)?;
write_tool_versions(
&preferred_project_file(&project),
&ToolVersions {
tools: [("nodejs".to_string(), "20.11.1".to_string())]
.into_iter()
.collect(),
},
)?;
let resolved = resolve_tool_version(&home, &project, None, "nodejs")?
.ok_or_else(|| std::io::Error::other("missing resolved version"))?;
assert_eq!(resolved.version, "20.11.1");
assert_eq!(resolved.scope, Scope::Project);
Ok(())
}
#[test]
fn parse_legacy_versions_should_support_tool_versions() -> Result<(), Box<dyn Error>> {
let versions =
parse_legacy_versions(Some(".tool-versions"), "nodejs 20.11.1\njava 21-tem\n")
.ok_or_else(|| std::io::Error::other("missing parsed legacy versions"))?;
assert_eq!(versions.tools.get("nodejs"), Some(&String::from("20.11.1")));
assert_eq!(versions.tools.get("java"), Some(&String::from("21-tem")));
Ok(())
}
#[test]
fn resolve_home_should_collect_legacy_candidates() -> Result<(), Box<dyn Error>> {
let temp_dir = TempDir::new()?;
let user_home = temp_dir.path().join("user");
fs::create_dir_all(user_home.join(".vfox"))?;
fs::create_dir_all(user_home.join(".version-fox"))?;
let layout = resolve_home_with(None, user_home)?;
assert_eq!(layout.migration_candidates.len(), 2);
assert!(
layout
.migration_candidates
.iter()
.any(|path| path.ends_with(".vfox"))
);
assert!(
layout
.migration_candidates
.iter()
.any(|path| path.ends_with(".version-fox"))
);
Ok(())
}
#[test]
fn set_app_config_value_should_validate_legacy_strategy_and_cache_duration()
-> Result<(), Box<dyn Error>> {
let mut config = AppConfig::default();
set_app_config_value(
&mut config,
"legacyVersionFile.strategy",
"latest_installed",
)?;
set_app_config_value(&mut config, "cache.availableHookDuration", "24h")?;
assert!(
set_app_config_value(&mut config, "legacyVersionFile.strategy", "unknown").is_err()
);
assert!(set_app_config_value(&mut config, "cache.availableHookDuration", "3w").is_err());
Ok(())
}
}