valheim_mod_manager/
config.rs1use config::{Config, ConfigError, File};
2use serde::{Deserialize, Serialize};
3use std::io::Write;
4use std::{fs::OpenOptions, path::Path};
5
6use crate::error::{AppError, AppResult};
7
8#[derive(Serialize, Deserialize)]
13pub struct AppConfig {
14 pub mod_list: Vec<String>,
16 pub log_level: String,
18 pub install_dir: Option<String>,
20}
21
22impl Default for AppConfig {
23 fn default() -> Self {
24 Self {
25 mod_list: vec![],
26 log_level: "error".into(),
27 install_dir: None,
28 }
29 }
30}
31
32#[cfg(not(tarpaulin_include))]
36pub static APP_CACHE_DIR: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| {
37 xdg::BaseDirectories::with_prefix("vmm")
38 .expect("Failed to initialize XDG base directories")
39 .get_config_home()
40 .to_string_lossy()
41 .into_owned()
42});
43
44pub fn get_config(config_override: Option<&Path>) -> Result<AppConfig, ConfigError> {
51 let default_config_data = AppConfig::default();
52
53 let mut builder = Config::builder()
54 .set_default("mod_list", default_config_data.mod_list.clone())?
55 .set_default("log_level", default_config_data.log_level.clone())?
56 .set_default("install_dir", default_config_data.install_dir.clone())?;
57
58 if let Some(path) = config_override {
59 builder = builder.add_source(File::with_name(path.to_str().unwrap_or_default()));
60 } else {
61 let local_path = Path::new("vmm_config.toml");
62 let xdg_dirs =
63 xdg::BaseDirectories::with_prefix("vmm").expect("Failed to initialize XDG base directories");
64 let global_path = xdg_dirs.get_config_file("vmm_config.toml");
65
66 if !local_path.exists() && !global_path.exists() {
67 if let Ok(path) = xdg_dirs.place_config_file("vmm_config.toml") {
68 let _ = create_missing_config_file(&path, &default_config_data);
69 }
70 }
71
72 if global_path.exists() {
73 builder = builder.add_source(File::with_name(global_path.to_str().unwrap_or_default()));
74 }
75
76 if local_path.exists() {
77 builder = builder.add_source(File::with_name("vmm_config.toml"));
78 }
79 }
80
81 builder.build()?.try_deserialize()
82}
83
84fn create_missing_config_file(
95 config_path: &Path,
96 default_config_data: &AppConfig,
97) -> AppResult<()> {
98 let serialized_config_data = toml::to_string(&default_config_data)
99 .map_err(|e| AppError::ConfigSerialization(format!("{}", e)))?;
100
101 let mut config_file = OpenOptions::new()
102 .write(true)
103 .create_new(true)
104 .open(config_path)?;
105
106 write!(config_file, "{}", serialized_config_data)?;
107
108 Ok(())
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use std::fs;
115 use tempfile::tempdir;
116
117 #[test]
118 fn test_default_config() {
119 let default_config = AppConfig::default();
120
121 assert!(default_config.mod_list.is_empty());
122 assert_eq!(default_config.log_level, "error");
123 }
124
125 #[test]
126 fn test_create_missing_config_file() {
127 let dir = tempdir().unwrap();
128 let config_path = dir.path().join("test_config.toml");
129 let default_config = AppConfig::default();
130
131 let result = create_missing_config_file(&config_path, &default_config);
132 assert!(result.is_ok());
133
134 assert!(config_path.exists());
135
136 let content = fs::read_to_string(&config_path).unwrap();
137 assert!(content.contains("mod_list"));
138 assert!(content.contains("log_level"));
139 assert!(content.contains("error"));
140
141 let parsed: AppConfig = toml::from_str(&content).unwrap();
142 assert_eq!(parsed.log_level, default_config.log_level);
143 assert_eq!(parsed.mod_list.len(), default_config.mod_list.len());
144 }
145
146 #[test]
147 fn test_custom_config_values() {
148 let custom_config = AppConfig {
149 mod_list: vec!["Owner1-ModA".to_string(), "Owner2-ModB".to_string()],
150 log_level: "debug".to_string(),
151 install_dir: Some("/path/to/install/directory".to_string()),
152 };
153
154 let dir = tempdir().unwrap();
155 let config_path = dir.path().join("custom_config.toml");
156
157 let result = create_missing_config_file(&config_path, &custom_config);
158 assert!(result.is_ok());
159
160 let content = fs::read_to_string(&config_path).unwrap();
161
162 assert!(content.contains("Owner1-ModA"));
163 assert!(content.contains("Owner2-ModB"));
164 assert!(content.contains("debug"));
165
166 let parsed: AppConfig = toml::from_str(&content).unwrap();
167 assert_eq!(parsed.log_level, "debug");
168 assert_eq!(parsed.mod_list.len(), 2);
169 assert_eq!(parsed.mod_list[0], "Owner1-ModA");
170 assert_eq!(parsed.mod_list[1], "Owner2-ModB");
171 }
172
173 #[test]
174 fn test_get_config_with_override() {
175 let dir = tempdir().unwrap();
176 let config_path = dir.path().join("override_config.toml");
177
178 let custom_config = AppConfig {
179 mod_list: vec!["Owner1-ModA".to_string()],
180 log_level: "debug".to_string(),
181 install_dir: None,
182 };
183
184 create_missing_config_file(&config_path, &custom_config).unwrap();
185
186 let loaded = get_config(Some(&config_path)).unwrap();
187 assert_eq!(loaded.log_level, "debug");
188 assert_eq!(loaded.mod_list, vec!["Owner1-ModA"]);
189 }
190}