1use std::fs;
4use std::path::{Path, PathBuf};
5
6use serde_yaml::Value;
7
8use crate::ConfigError;
9use crate::types::{AppConfig, ToolVersions};
10
11pub 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
25pub 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
37pub 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
46pub 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
59pub 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
105pub 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
163pub 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
195pub 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}