1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::fs;
6use std::path::PathBuf;
7use thiserror::Error;
8
9#[derive(Error, Debug)]
10pub enum ConfigError {
11 #[error("IO error: {0}")]
12 Io(#[from] std::io::Error),
13 #[error("TOML serialization error: {0}")]
14 TomlSerialize(#[from] toml::ser::Error),
15 #[error("TOML deserialization error: {0}")]
16 TomlDeserialize(#[from] toml::de::Error),
17 #[error("Configuration key '{key}' not found")]
18 KeyNotFound { key: String },
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize, Default)]
22pub struct NgdpConfig {
23 #[serde(flatten)]
25 pub defaults: DefaultConfig,
26 #[serde(default)]
28 pub custom: HashMap<String, String>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct DefaultConfig {
33 pub default_region: String,
34 pub cache_dir: String,
35 pub timeout: u32,
36 pub cache_enabled: bool,
37 pub cache_ttl: u32,
38 pub max_concurrent_downloads: u32,
39 pub user_agent: String,
40 pub verify_certificates: bool,
41 pub proxy_url: String,
42 pub ribbit_timeout: u32,
43 pub tact_timeout: u32,
44 pub retry_attempts: u32,
45 pub log_file: String,
46 pub color_output: bool,
47 pub fallback_to_tact: bool,
48 pub use_community_cdn_fallbacks: bool,
49 pub custom_cdn_fallbacks: String,
50}
51
52impl Default for DefaultConfig {
53 fn default() -> Self {
54 Self {
55 default_region: "us".to_string(),
56 cache_dir: "~/.cache/ngdp".to_string(),
57 timeout: 30,
58 cache_enabled: true,
59 cache_ttl: 1800, max_concurrent_downloads: 4,
61 user_agent: "ngdp-client/0.1.2".to_string(),
62 verify_certificates: true,
63 proxy_url: String::new(),
64 ribbit_timeout: 30,
65 tact_timeout: 30,
66 retry_attempts: 3,
67 log_file: String::new(),
68 color_output: true,
69 fallback_to_tact: true,
70 use_community_cdn_fallbacks: true,
71 custom_cdn_fallbacks: String::new(),
72 }
73 }
74}
75
76pub struct ConfigManager {
77 config_path: PathBuf,
78 config: NgdpConfig,
79}
80
81impl ConfigManager {
82 pub fn new() -> Result<Self, ConfigError> {
84 let config_path = Self::get_config_path()?;
85 let config = Self::load_config(&config_path)?;
86
87 Ok(Self {
88 config_path,
89 config,
90 })
91 }
92
93 fn get_config_path() -> Result<PathBuf, ConfigError> {
95 let config_dir = dirs::config_dir()
96 .unwrap_or_else(|| PathBuf::from("."))
97 .join("cascette");
98
99 if !config_dir.exists() {
101 fs::create_dir_all(&config_dir)?;
102 }
103
104 Ok(config_dir.join("ngdp-client.toml"))
105 }
106
107 fn load_config(config_path: &PathBuf) -> Result<NgdpConfig, ConfigError> {
109 if config_path.exists() {
110 let content = fs::read_to_string(config_path)?;
111 let config: NgdpConfig = toml::from_str(&content)?;
112 Ok(config)
113 } else {
114 let config = NgdpConfig::default();
116 Self::save_config_to_file(config_path, &config)?;
117 Ok(config)
118 }
119 }
120
121 fn save_config_to_file(config_path: &PathBuf, config: &NgdpConfig) -> Result<(), ConfigError> {
123 let toml_content = toml::to_string_pretty(config)?;
124 fs::write(config_path, toml_content)?;
125 Ok(())
126 }
127
128 pub fn save(&self) -> Result<(), ConfigError> {
130 Self::save_config_to_file(&self.config_path, &self.config)
131 }
132
133 pub fn get(&self, key: &str) -> Result<String, ConfigError> {
135 if let Some(value) = self.config.custom.get(key) {
137 return Ok(value.clone());
138 }
139
140 let value = match key {
142 "default_region" => &self.config.defaults.default_region,
143 "cache_dir" => &self.config.defaults.cache_dir,
144 "timeout" => return Ok(self.config.defaults.timeout.to_string()),
145 "cache_enabled" => return Ok(self.config.defaults.cache_enabled.to_string()),
146 "cache_ttl" => return Ok(self.config.defaults.cache_ttl.to_string()),
147 "max_concurrent_downloads" => {
148 return Ok(self.config.defaults.max_concurrent_downloads.to_string());
149 }
150 "user_agent" => &self.config.defaults.user_agent,
151 "verify_certificates" => {
152 return Ok(self.config.defaults.verify_certificates.to_string());
153 }
154 "proxy_url" => &self.config.defaults.proxy_url,
155 "ribbit_timeout" => return Ok(self.config.defaults.ribbit_timeout.to_string()),
156 "tact_timeout" => return Ok(self.config.defaults.tact_timeout.to_string()),
157 "retry_attempts" => return Ok(self.config.defaults.retry_attempts.to_string()),
158 "log_file" => &self.config.defaults.log_file,
159 "color_output" => return Ok(self.config.defaults.color_output.to_string()),
160 "fallback_to_tact" => return Ok(self.config.defaults.fallback_to_tact.to_string()),
161 "use_community_cdn_fallbacks" => {
162 return Ok(self.config.defaults.use_community_cdn_fallbacks.to_string());
163 }
164 "custom_cdn_fallbacks" => &self.config.defaults.custom_cdn_fallbacks,
165 _ => {
166 return Err(ConfigError::KeyNotFound {
167 key: key.to_string(),
168 });
169 }
170 };
171
172 Ok(value.clone())
173 }
174
175 pub fn set(&mut self, key: String, value: String) -> Result<(), ConfigError> {
177 self.config.custom.insert(key, value);
179 self.save()?;
180 Ok(())
181 }
182
183 pub fn get_all(&self) -> HashMap<String, String> {
185 let mut all_config = HashMap::new();
186
187 all_config.insert(
189 "default_region".to_string(),
190 self.config.defaults.default_region.clone(),
191 );
192 all_config.insert(
193 "cache_dir".to_string(),
194 self.config.defaults.cache_dir.clone(),
195 );
196 all_config.insert(
197 "timeout".to_string(),
198 self.config.defaults.timeout.to_string(),
199 );
200 all_config.insert(
201 "cache_enabled".to_string(),
202 self.config.defaults.cache_enabled.to_string(),
203 );
204 all_config.insert(
205 "cache_ttl".to_string(),
206 self.config.defaults.cache_ttl.to_string(),
207 );
208 all_config.insert(
209 "max_concurrent_downloads".to_string(),
210 self.config.defaults.max_concurrent_downloads.to_string(),
211 );
212 all_config.insert(
213 "user_agent".to_string(),
214 self.config.defaults.user_agent.clone(),
215 );
216 all_config.insert(
217 "verify_certificates".to_string(),
218 self.config.defaults.verify_certificates.to_string(),
219 );
220 all_config.insert(
221 "proxy_url".to_string(),
222 self.config.defaults.proxy_url.clone(),
223 );
224 all_config.insert(
225 "ribbit_timeout".to_string(),
226 self.config.defaults.ribbit_timeout.to_string(),
227 );
228 all_config.insert(
229 "tact_timeout".to_string(),
230 self.config.defaults.tact_timeout.to_string(),
231 );
232 all_config.insert(
233 "retry_attempts".to_string(),
234 self.config.defaults.retry_attempts.to_string(),
235 );
236 all_config.insert(
237 "log_file".to_string(),
238 self.config.defaults.log_file.clone(),
239 );
240 all_config.insert(
241 "color_output".to_string(),
242 self.config.defaults.color_output.to_string(),
243 );
244 all_config.insert(
245 "fallback_to_tact".to_string(),
246 self.config.defaults.fallback_to_tact.to_string(),
247 );
248 all_config.insert(
249 "use_community_cdn_fallbacks".to_string(),
250 self.config.defaults.use_community_cdn_fallbacks.to_string(),
251 );
252 all_config.insert(
253 "custom_cdn_fallbacks".to_string(),
254 self.config.defaults.custom_cdn_fallbacks.clone(),
255 );
256
257 for (key, value) in &self.config.custom {
259 all_config.insert(key.clone(), value.clone());
260 }
261
262 all_config
263 }
264
265 pub fn reset(&mut self) -> Result<(), ConfigError> {
267 self.config = NgdpConfig::default();
268 self.save()?;
269 Ok(())
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use std::env;
277 use tempfile::TempDir;
278
279 #[test]
280 fn test_config_creation_and_persistence() {
281 let temp_dir = TempDir::new().unwrap();
282 let config_path = temp_dir.path().join("test-config.toml");
283
284 let mut config = NgdpConfig::default();
286 config
287 .custom
288 .insert("test.key".to_string(), "test_value".to_string());
289
290 ConfigManager::save_config_to_file(&config_path, &config).unwrap();
292
293 let loaded_config = ConfigManager::load_config(&config_path).unwrap();
295
296 assert_eq!(loaded_config.custom.get("test.key").unwrap(), "test_value");
297 assert_eq!(loaded_config.defaults.default_region, "us");
298 }
299
300 #[test]
301 fn test_config_get_set() {
302 let temp_dir = TempDir::new().unwrap();
304 unsafe {
307 env::set_var("XDG_CONFIG_HOME", temp_dir.path());
308 }
309
310 let mut manager = ConfigManager::new().unwrap();
311
312 manager
314 .set("test.product".to_string(), "wow_classic_era".to_string())
315 .unwrap();
316 let value = manager.get("test.product").unwrap();
317 assert_eq!(value, "wow_classic_era");
318
319 let default_region = manager.get("default_region").unwrap();
321 assert_eq!(default_region, "us");
322
323 let result = manager.get("nonexistent.key");
325 assert!(result.is_err());
326 }
327}