1use crate::domain_types::{ModuleName, Version};
18use crate::error::{DissolveError, Result};
19use crate::types::TypeIntrospectionMethod;
20use serde::{Deserialize, Serialize};
21use std::collections::HashMap;
22use std::path::{Path, PathBuf};
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct Config {
27 pub type_introspection: TypeIntrospectionMethod,
29
30 pub scan_paths: Vec<PathBuf>,
32
33 pub excluded_modules: Vec<ModuleName>,
35
36 pub write_changes: bool,
38
39 pub create_backups: bool,
41
42 pub current_version: Option<Version>,
44
45 pub timeout: TimeoutConfig,
47
48 pub performance: PerformanceConfig,
50
51 pub output: OutputConfig,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct TimeoutConfig {
58 pub lsp_timeout: u64,
60
61 pub file_timeout: u64,
63
64 pub type_query_timeout: u64,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct PerformanceConfig {
71 pub parallel_workers: usize,
73
74 pub cache_asts: bool,
76
77 pub string_interning: bool,
79
80 pub batch_size: usize,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct OutputConfig {
87 pub show_progress: bool,
89
90 pub verbosity: u8,
92
93 pub colorize: bool,
95
96 pub show_stats: bool,
98}
99
100impl Default for Config {
101 fn default() -> Self {
102 Self {
103 type_introspection: TypeIntrospectionMethod::PyrightLsp,
104 scan_paths: vec![PathBuf::from(".")],
105 excluded_modules: vec![],
106 write_changes: false,
107 create_backups: true,
108 current_version: None,
109 timeout: TimeoutConfig::default(),
110 performance: PerformanceConfig::default(),
111 output: OutputConfig::default(),
112 }
113 }
114}
115
116impl Default for TimeoutConfig {
117 fn default() -> Self {
118 Self {
119 lsp_timeout: 30,
120 file_timeout: 10,
121 type_query_timeout: 5,
122 }
123 }
124}
125
126impl Default for PerformanceConfig {
127 fn default() -> Self {
128 Self {
129 parallel_workers: num_cpus::get(),
130 cache_asts: true,
131 string_interning: true,
132 batch_size: 100,
133 }
134 }
135}
136
137impl Default for OutputConfig {
138 fn default() -> Self {
139 Self {
140 show_progress: true,
141 verbosity: 1,
142 colorize: atty::is(atty::Stream::Stdout),
143 show_stats: true,
144 }
145 }
146}
147
148impl Config {
149 pub fn new() -> Self {
151 Self::default()
152 }
153
154 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
156 let content = std::fs::read_to_string(&path).map_err(|e| {
157 DissolveError::config_error(format!(
158 "Failed to read config file {}: {}",
159 path.as_ref().display(),
160 e
161 ))
162 })?;
163
164 let config: Config = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("toml") {
165 toml::from_str(&content).map_err(|e| {
166 DissolveError::config_error(format!("Failed to parse TOML config: {}", e))
167 })?
168 } else {
169 serde_json::from_str(&content).map_err(|e| {
170 DissolveError::config_error(format!("Failed to parse JSON config: {}", e))
171 })?
172 };
173
174 Ok(config)
175 }
176
177 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
179 let content = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("toml") {
180 toml::to_string_pretty(self).map_err(|e| {
181 DissolveError::config_error(format!("Failed to serialize to TOML: {}", e))
182 })?
183 } else {
184 serde_json::to_string_pretty(self).map_err(|e| {
185 DissolveError::config_error(format!("Failed to serialize to JSON: {}", e))
186 })?
187 };
188
189 std::fs::write(&path, content).map_err(|e| {
190 DissolveError::config_error(format!(
191 "Failed to write config file {}: {}",
192 path.as_ref().display(),
193 e
194 ))
195 })?;
196
197 Ok(())
198 }
199
200 pub fn load_merged(
206 config_file: Option<&Path>,
207 env_overrides: &HashMap<String, String>,
208 cli_overrides: &CliOverrides,
209 ) -> Result<Self> {
210 let mut config = if let Some(config_path) = config_file {
211 if config_path.exists() {
212 Self::from_file(config_path)?
213 } else {
214 Self::default()
215 }
216 } else {
217 Self::default()
218 };
219
220 config.apply_env_overrides(env_overrides)?;
222
223 config.apply_cli_overrides(cli_overrides);
225
226 Ok(config)
227 }
228
229 fn apply_env_overrides(&mut self, env_vars: &HashMap<String, String>) -> Result<()> {
231 if let Some(timeout) = env_vars.get("DISSOLVE_LSP_TIMEOUT") {
232 self.timeout.lsp_timeout = timeout
233 .parse()
234 .map_err(|_| DissolveError::config_error("Invalid LSP timeout value"))?;
235 }
236
237 if let Some(workers) = env_vars.get("DISSOLVE_PARALLEL_WORKERS") {
238 self.performance.parallel_workers = workers
239 .parse()
240 .map_err(|_| DissolveError::config_error("Invalid parallel workers value"))?;
241 }
242
243 if let Some(verbosity) = env_vars.get("DISSOLVE_VERBOSITY") {
244 self.output.verbosity = verbosity
245 .parse()
246 .map_err(|_| DissolveError::config_error("Invalid verbosity value"))?;
247 }
248
249 Ok(())
250 }
251
252 fn apply_cli_overrides(&mut self, overrides: &CliOverrides) {
254 if let Some(type_method) = overrides.type_introspection {
255 self.type_introspection = type_method;
256 }
257
258 if let Some(write) = overrides.write_changes {
259 self.write_changes = write;
260 }
261
262 if let Some(verbosity) = overrides.verbosity {
263 self.output.verbosity = verbosity;
264 }
265
266 if let Some(no_color) = overrides.no_color {
267 self.output.colorize = !no_color;
268 }
269 }
270
271 pub fn validate(&self) -> Result<()> {
273 if self.timeout.lsp_timeout == 0 {
274 return Err(DissolveError::config_error(
275 "LSP timeout must be greater than 0",
276 ));
277 }
278
279 if self.performance.parallel_workers == 0 {
280 return Err(DissolveError::config_error(
281 "Parallel workers must be greater than 0",
282 ));
283 }
284
285 if self.output.verbosity > 3 {
286 return Err(DissolveError::config_error("Verbosity level must be 0-3"));
287 }
288
289 Ok(())
290 }
291}
292
293#[derive(Debug, Default)]
295pub struct CliOverrides {
296 pub type_introspection: Option<TypeIntrospectionMethod>,
297 pub write_changes: Option<bool>,
298 pub verbosity: Option<u8>,
299 pub no_color: Option<bool>,
300}
301
302pub struct ConfigBuilder {
304 config: Config,
305}
306
307impl ConfigBuilder {
308 pub fn new() -> Self {
309 Self {
310 config: Config::default(),
311 }
312 }
313
314 pub fn type_introspection(mut self, method: TypeIntrospectionMethod) -> Self {
315 self.config.type_introspection = method;
316 self
317 }
318
319 pub fn scan_paths(mut self, paths: Vec<PathBuf>) -> Self {
320 self.config.scan_paths = paths;
321 self
322 }
323
324 pub fn write_changes(mut self, write: bool) -> Self {
325 self.config.write_changes = write;
326 self
327 }
328
329 pub fn current_version(mut self, version: Version) -> Self {
330 self.config.current_version = Some(version);
331 self
332 }
333
334 pub fn parallel_workers(mut self, workers: usize) -> Self {
335 self.config.performance.parallel_workers = workers;
336 self
337 }
338
339 pub fn verbosity(mut self, level: u8) -> Self {
340 self.config.output.verbosity = level;
341 self
342 }
343
344 pub fn build(self) -> Result<Config> {
345 self.config.validate()?;
346 Ok(self.config)
347 }
348}
349
350impl Default for ConfigBuilder {
351 fn default() -> Self {
352 Self::new()
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359 use tempfile::TempDir;
360
361 #[test]
362 fn test_config_default() {
363 let config = Config::default();
364 assert_eq!(
365 config.type_introspection,
366 TypeIntrospectionMethod::PyrightLsp
367 );
368 assert!(!config.write_changes);
369 assert!(config.create_backups);
370 }
371
372 #[test]
373 fn test_config_builder() {
374 let config = ConfigBuilder::new()
375 .type_introspection(TypeIntrospectionMethod::MypyDaemon)
376 .write_changes(true)
377 .verbosity(2)
378 .build()
379 .unwrap();
380
381 assert_eq!(
382 config.type_introspection,
383 TypeIntrospectionMethod::MypyDaemon
384 );
385 assert!(config.write_changes);
386 assert_eq!(config.output.verbosity, 2);
387 }
388
389 #[test]
390 fn test_config_file_operations() {
391 let temp_dir = TempDir::new().unwrap();
392 let config_path = temp_dir.path().join("test_config.json");
393
394 let config = ConfigBuilder::new()
395 .verbosity(3)
396 .write_changes(true)
397 .build()
398 .unwrap();
399
400 config.save_to_file(&config_path).unwrap();
401
402 let loaded_config = Config::from_file(&config_path).unwrap();
403 assert_eq!(loaded_config.output.verbosity, 3);
404 assert!(loaded_config.write_changes);
405 }
406
407 #[test]
408 fn test_config_validation() {
409 let mut config = Config::default();
410 config.timeout.lsp_timeout = 0;
411
412 assert!(config.validate().is_err());
413 }
414}