Skip to main content

vs_config/
config_file.rs

1//! Reading and writing persisted `vs` configuration files.
2
3use std::fs;
4use std::path::{Path, PathBuf};
5
6use serde_yaml::Value;
7
8use crate::ConfigError;
9use crate::types::{AppConfig, ToolVersions};
10
11/// Reads `config.yaml` from the active home.
12pub fn read_app_config(home: &Path) -> Result<AppConfig, ConfigError> {
13    let path = home.join("config.yaml");
14    if !path.exists() {
15        return Ok(AppConfig::default());
16    }
17
18    let content = fs::read_to_string(&path)?;
19    serde_yaml::from_str(&content).map_err(|error| ConfigError::Yaml {
20        path,
21        message: error.to_string(),
22    })
23}
24
25/// Writes `config.yaml` to the active home.
26pub fn write_app_config(home: &Path, config: &AppConfig) -> Result<(), ConfigError> {
27    fs::create_dir_all(home)?;
28    let path = home.join("config.yaml");
29    let rendered = serde_yaml::to_string(config).map_err(|error| ConfigError::Yaml {
30        path: path.clone(),
31        message: error.to_string(),
32    })?;
33    fs::write(path, rendered)?;
34    Ok(())
35}
36
37/// Reads a TOML tool file.
38pub fn read_tool_versions(path: &Path) -> Result<ToolVersions, ConfigError> {
39    let content = fs::read_to_string(path)?;
40    toml::from_str(&content).map_err(|error| ConfigError::Toml {
41        path: path.to_path_buf(),
42        message: error.to_string(),
43    })
44}
45
46/// Writes a TOML tool file.
47pub fn write_tool_versions(path: &Path, tools: &ToolVersions) -> Result<(), ConfigError> {
48    if let Some(parent) = path.parent() {
49        fs::create_dir_all(parent)?;
50    }
51    let rendered = toml::to_string_pretty(tools).map_err(|error| ConfigError::Toml {
52        path: path.to_path_buf(),
53        message: error.to_string(),
54    })?;
55    fs::write(path, rendered)?;
56    Ok(())
57}
58
59/// Lists user-visible configuration values as strings.
60pub fn flatten_app_config(config: &AppConfig) -> Vec<(String, String)> {
61    vec![
62        (
63            String::from("proxy.enable"),
64            config.proxy.enable.to_string(),
65        ),
66        (
67            String::from("proxy.url"),
68            if config.proxy.url.is_empty() {
69                String::from("<unset>")
70            } else {
71                config.proxy.url.clone()
72            },
73        ),
74        (
75            String::from("storage.sdkPath"),
76            if config.storage.sdk_path.is_empty() {
77                String::from("<unset>")
78            } else {
79                config.storage.sdk_path.clone()
80            },
81        ),
82        (
83            String::from("registry.address"),
84            if config.registry.address.is_empty() {
85                String::from("<unset>")
86            } else {
87                config.registry.address.clone()
88            },
89        ),
90        (
91            String::from("legacyVersionFile.enable"),
92            config.legacy_version_file.enable.to_string(),
93        ),
94        (
95            String::from("legacyVersionFile.strategy"),
96            config.legacy_version_file.strategy.clone(),
97        ),
98        (
99            String::from("cache.availableHookDuration"),
100            config.cache.available_hook_duration.clone(),
101        ),
102    ]
103}
104
105/// Sets a top-level config value by key.
106pub fn set_app_config_value(
107    config: &mut AppConfig,
108    key: &str,
109    value: &str,
110) -> Result<(), ConfigError> {
111    match key {
112        "proxy.enable" => {
113            config.proxy.enable = value
114                .parse::<bool>()
115                .map_err(|_| ConfigError::InvalidValue {
116                    key: key.to_string(),
117                    value: value.to_string(),
118                })?;
119        }
120        "proxy.url" => {
121            config.proxy.url = value.to_string();
122        }
123        "storage.sdkPath" => {
124            config.storage.sdk_path = value.to_string();
125        }
126        "registry.address" => {
127            config.registry.address = value.to_string();
128        }
129        "legacyVersionFile.enable" => {
130            config.legacy_version_file.enable =
131                value
132                    .parse::<bool>()
133                    .map_err(|_| ConfigError::InvalidValue {
134                        key: key.to_string(),
135                        value: value.to_string(),
136                    })?;
137        }
138        "legacyVersionFile.strategy" => {
139            if !is_supported_legacy_strategy(value) {
140                return Err(ConfigError::InvalidValue {
141                    key: key.to_string(),
142                    value: value.to_string(),
143                });
144            }
145            config.legacy_version_file.strategy = value.to_string();
146        }
147        "cache.availableHookDuration" => {
148            if !is_supported_duration(value) {
149                return Err(ConfigError::InvalidValue {
150                    key: key.to_string(),
151                    value: value.to_string(),
152                });
153            }
154            config.cache.available_hook_duration = value.to_string();
155        }
156        _ => {
157            return Err(ConfigError::UnknownKey(key.to_string()));
158        }
159    }
160    Ok(())
161}
162
163/// Unsets a top-level config value by key.
164pub fn unset_app_config_value(config: &mut AppConfig, key: &str) -> Result<(), ConfigError> {
165    match key {
166        "proxy.enable" => {
167            config.proxy.enable = AppConfig::default().proxy.enable;
168        }
169        "proxy.url" => {
170            config.proxy.url.clear();
171        }
172        "storage.sdkPath" => {
173            config.storage.sdk_path.clear();
174        }
175        "registry.address" => {
176            config.registry.address.clear();
177        }
178        "legacyVersionFile.enable" => {
179            config.legacy_version_file.enable = AppConfig::default().legacy_version_file.enable;
180        }
181        "legacyVersionFile.strategy" => {
182            config.legacy_version_file.strategy = AppConfig::default().legacy_version_file.strategy;
183        }
184        "cache.availableHookDuration" => {
185            config.cache.available_hook_duration =
186                AppConfig::default().cache.available_hook_duration;
187        }
188        _ => {
189            return Err(ConfigError::UnknownKey(key.to_string()));
190        }
191    }
192    Ok(())
193}
194
195/// Converts an application config to a YAML value for debugging.
196pub fn app_config_to_value(config: &AppConfig) -> Result<Value, ConfigError> {
197    serde_yaml::to_value(config).map_err(|error| ConfigError::Yaml {
198        path: PathBuf::from("config.yaml"),
199        message: error.to_string(),
200    })
201}
202
203fn is_supported_legacy_strategy(value: &str) -> bool {
204    matches!(value, "specified" | "latest_installed" | "latest_available")
205}
206
207fn is_supported_duration(value: &str) -> bool {
208    let trimmed = value.trim();
209    if trimmed.is_empty() || trimmed == "0" {
210        return true;
211    }
212
213    let split_at = trimmed
214        .find(|ch: char| !ch.is_ascii_digit())
215        .unwrap_or(trimmed.len());
216    let (amount, unit) = trimmed.split_at(split_at);
217    if amount
218        .parse::<u64>()
219        .ok()
220        .filter(|amount| *amount > 0)
221        .is_none()
222    {
223        return false;
224    }
225
226    matches!(unit, "" | "s" | "m" | "h" | "d")
227}