1mod config_file;
4mod home;
5mod legacy;
6mod project;
7mod resolution;
8mod types;
9
10use std::path::PathBuf;
11
12use thiserror::Error;
13
14pub use config_file::{
15 app_config_to_value, flatten_app_config, read_app_config, read_tool_versions,
16 set_app_config_value, unset_app_config_value, write_app_config, write_tool_versions,
17};
18pub use home::{resolve_home, resolve_home_with};
19pub use legacy::{
20 find_legacy_file, parse_legacy_versions, read_legacy_versions, supported_legacy_files,
21};
22pub use project::{find_project_file, preferred_project_file};
23pub use resolution::{global_tools_file, resolve_tool_version, session_tools_file};
24pub use types::{
25 AppConfig, CacheConfig, HomeLayout, LegacyVersionFileConfig, ProxyConfig, RegistryConfig,
26 ResolvedToolVersion, Scope, StorageConfig, ToolVersions,
27};
28
29#[derive(Debug, Error)]
31pub enum ConfigError {
32 #[error("failed to resolve the user home directory")]
34 HomeDirectoryUnavailable,
35 #[error(transparent)]
37 Io(#[from] std::io::Error),
38 #[error("failed to parse TOML file at {path}: {message}")]
40 Toml { path: PathBuf, message: String },
41 #[error("failed to parse YAML file at {path}: {message}")]
43 Yaml { path: PathBuf, message: String },
44 #[error("unknown config key: {0}")]
46 UnknownKey(String),
47 #[error("invalid value {value} for key {key}")]
49 InvalidValue { key: String, value: String },
50 #[error("unsupported legacy file: {0}")]
52 UnsupportedLegacyFile(PathBuf),
53}
54
55#[cfg(test)]
56mod tests {
57 use std::error::Error;
58 use std::fs;
59
60 use tempfile::TempDir;
61
62 use super::*;
63
64 #[test]
65 fn resolve_home_should_prefer_vs_home() -> Result<(), Box<dyn Error>> {
66 let temp_dir = TempDir::new()?;
67 let explicit_home = temp_dir.path().join("explicit");
68 let layout = resolve_home_with(Some(explicit_home.clone()), temp_dir.path().join("user"))?;
69
70 assert_eq!(layout.active_home, explicit_home);
71 assert!(layout.migration_candidates.is_empty());
72 Ok(())
73 }
74
75 #[test]
76 fn resolve_tool_version_should_prefer_project_config_over_global() -> Result<(), Box<dyn Error>>
77 {
78 let temp_dir = TempDir::new()?;
79 let home = temp_dir.path().join("home");
80 fs::create_dir_all(home.join("global"))?;
81 write_tool_versions(
82 &global_tools_file(&home),
83 &ToolVersions {
84 tools: [("nodejs".to_string(), "18.19.0".to_string())]
85 .into_iter()
86 .collect(),
87 },
88 )?;
89
90 let project = temp_dir.path().join("project");
91 fs::create_dir_all(&project)?;
92 write_tool_versions(
93 &preferred_project_file(&project),
94 &ToolVersions {
95 tools: [("nodejs".to_string(), "20.11.1".to_string())]
96 .into_iter()
97 .collect(),
98 },
99 )?;
100
101 let resolved = resolve_tool_version(&home, &project, None, "nodejs")?
102 .ok_or_else(|| std::io::Error::other("missing resolved version"))?;
103
104 assert_eq!(resolved.version, "20.11.1");
105 assert_eq!(resolved.scope, Scope::Project);
106 Ok(())
107 }
108
109 #[test]
110 fn parse_legacy_versions_should_support_tool_versions() -> Result<(), Box<dyn Error>> {
111 let versions =
112 parse_legacy_versions(Some(".tool-versions"), "nodejs 20.11.1\njava 21-tem\n")
113 .ok_or_else(|| std::io::Error::other("missing parsed legacy versions"))?;
114
115 assert_eq!(versions.tools.get("nodejs"), Some(&String::from("20.11.1")));
116 assert_eq!(versions.tools.get("java"), Some(&String::from("21-tem")));
117 Ok(())
118 }
119
120 #[test]
121 fn resolve_home_should_collect_legacy_candidates() -> Result<(), Box<dyn Error>> {
122 let temp_dir = TempDir::new()?;
123 let user_home = temp_dir.path().join("user");
124 fs::create_dir_all(user_home.join(".vfox"))?;
125 fs::create_dir_all(user_home.join(".version-fox"))?;
126
127 let layout = resolve_home_with(None, user_home)?;
128
129 assert_eq!(layout.migration_candidates.len(), 2);
130 assert!(
131 layout
132 .migration_candidates
133 .iter()
134 .any(|path| path.ends_with(".vfox"))
135 );
136 assert!(
137 layout
138 .migration_candidates
139 .iter()
140 .any(|path| path.ends_with(".version-fox"))
141 );
142 Ok(())
143 }
144
145 #[test]
146 fn set_app_config_value_should_validate_legacy_strategy_and_cache_duration()
147 -> Result<(), Box<dyn Error>> {
148 let mut config = AppConfig::default();
149 set_app_config_value(
150 &mut config,
151 "legacyVersionFile.strategy",
152 "latest_installed",
153 )?;
154 set_app_config_value(&mut config, "cache.availableHookDuration", "24h")?;
155
156 assert!(
157 set_app_config_value(&mut config, "legacyVersionFile.strategy", "unknown").is_err()
158 );
159 assert!(set_app_config_value(&mut config, "cache.availableHookDuration", "3w").is_err());
160 Ok(())
161 }
162}