use crate::domain_types::{ModuleName, Version};
use crate::error::{DissolveError, Result};
use crate::types::TypeIntrospectionMethod;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub type_introspection: TypeIntrospectionMethod,
pub scan_paths: Vec<PathBuf>,
pub excluded_modules: Vec<ModuleName>,
pub write_changes: bool,
pub create_backups: bool,
pub current_version: Option<Version>,
pub timeout: TimeoutConfig,
pub performance: PerformanceConfig,
pub output: OutputConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeoutConfig {
pub lsp_timeout: u64,
pub file_timeout: u64,
pub type_query_timeout: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceConfig {
pub parallel_workers: usize,
pub cache_asts: bool,
pub string_interning: bool,
pub batch_size: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputConfig {
pub show_progress: bool,
pub verbosity: u8,
pub colorize: bool,
pub show_stats: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
type_introspection: TypeIntrospectionMethod::PyrightLsp,
scan_paths: vec![PathBuf::from(".")],
excluded_modules: vec![],
write_changes: false,
create_backups: true,
current_version: None,
timeout: TimeoutConfig::default(),
performance: PerformanceConfig::default(),
output: OutputConfig::default(),
}
}
}
impl Default for TimeoutConfig {
fn default() -> Self {
Self {
lsp_timeout: 30,
file_timeout: 10,
type_query_timeout: 5,
}
}
}
impl Default for PerformanceConfig {
fn default() -> Self {
Self {
parallel_workers: num_cpus::get(),
cache_asts: true,
string_interning: true,
batch_size: 100,
}
}
}
impl Default for OutputConfig {
fn default() -> Self {
Self {
show_progress: true,
verbosity: 1,
colorize: atty::is(atty::Stream::Stdout),
show_stats: true,
}
}
}
impl Config {
pub fn new() -> Self {
Self::default()
}
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let content = std::fs::read_to_string(&path).map_err(|e| {
DissolveError::config_error(format!(
"Failed to read config file {}: {}",
path.as_ref().display(),
e
))
})?;
let config: Config = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("toml") {
toml::from_str(&content).map_err(|e| {
DissolveError::config_error(format!("Failed to parse TOML config: {}", e))
})?
} else {
serde_json::from_str(&content).map_err(|e| {
DissolveError::config_error(format!("Failed to parse JSON config: {}", e))
})?
};
Ok(config)
}
pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let content = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("toml") {
toml::to_string_pretty(self).map_err(|e| {
DissolveError::config_error(format!("Failed to serialize to TOML: {}", e))
})?
} else {
serde_json::to_string_pretty(self).map_err(|e| {
DissolveError::config_error(format!("Failed to serialize to JSON: {}", e))
})?
};
std::fs::write(&path, content).map_err(|e| {
DissolveError::config_error(format!(
"Failed to write config file {}: {}",
path.as_ref().display(),
e
))
})?;
Ok(())
}
pub fn load_merged(
config_file: Option<&Path>,
env_overrides: &HashMap<String, String>,
cli_overrides: &CliOverrides,
) -> Result<Self> {
let mut config = if let Some(config_path) = config_file {
if config_path.exists() {
Self::from_file(config_path)?
} else {
Self::default()
}
} else {
Self::default()
};
config.apply_env_overrides(env_overrides)?;
config.apply_cli_overrides(cli_overrides);
Ok(config)
}
fn apply_env_overrides(&mut self, env_vars: &HashMap<String, String>) -> Result<()> {
if let Some(timeout) = env_vars.get("DISSOLVE_LSP_TIMEOUT") {
self.timeout.lsp_timeout = timeout
.parse()
.map_err(|_| DissolveError::config_error("Invalid LSP timeout value"))?;
}
if let Some(workers) = env_vars.get("DISSOLVE_PARALLEL_WORKERS") {
self.performance.parallel_workers = workers
.parse()
.map_err(|_| DissolveError::config_error("Invalid parallel workers value"))?;
}
if let Some(verbosity) = env_vars.get("DISSOLVE_VERBOSITY") {
self.output.verbosity = verbosity
.parse()
.map_err(|_| DissolveError::config_error("Invalid verbosity value"))?;
}
Ok(())
}
fn apply_cli_overrides(&mut self, overrides: &CliOverrides) {
if let Some(type_method) = overrides.type_introspection {
self.type_introspection = type_method;
}
if let Some(write) = overrides.write_changes {
self.write_changes = write;
}
if let Some(verbosity) = overrides.verbosity {
self.output.verbosity = verbosity;
}
if let Some(no_color) = overrides.no_color {
self.output.colorize = !no_color;
}
}
pub fn validate(&self) -> Result<()> {
if self.timeout.lsp_timeout == 0 {
return Err(DissolveError::config_error(
"LSP timeout must be greater than 0",
));
}
if self.performance.parallel_workers == 0 {
return Err(DissolveError::config_error(
"Parallel workers must be greater than 0",
));
}
if self.output.verbosity > 3 {
return Err(DissolveError::config_error("Verbosity level must be 0-3"));
}
Ok(())
}
}
#[derive(Debug, Default)]
pub struct CliOverrides {
pub type_introspection: Option<TypeIntrospectionMethod>,
pub write_changes: Option<bool>,
pub verbosity: Option<u8>,
pub no_color: Option<bool>,
}
pub struct ConfigBuilder {
config: Config,
}
impl ConfigBuilder {
pub fn new() -> Self {
Self {
config: Config::default(),
}
}
pub fn type_introspection(mut self, method: TypeIntrospectionMethod) -> Self {
self.config.type_introspection = method;
self
}
pub fn scan_paths(mut self, paths: Vec<PathBuf>) -> Self {
self.config.scan_paths = paths;
self
}
pub fn write_changes(mut self, write: bool) -> Self {
self.config.write_changes = write;
self
}
pub fn current_version(mut self, version: Version) -> Self {
self.config.current_version = Some(version);
self
}
pub fn parallel_workers(mut self, workers: usize) -> Self {
self.config.performance.parallel_workers = workers;
self
}
pub fn verbosity(mut self, level: u8) -> Self {
self.config.output.verbosity = level;
self
}
pub fn build(self) -> Result<Config> {
self.config.validate()?;
Ok(self.config)
}
}
impl Default for ConfigBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_config_default() {
let config = Config::default();
assert_eq!(
config.type_introspection,
TypeIntrospectionMethod::PyrightLsp
);
assert!(!config.write_changes);
assert!(config.create_backups);
}
#[test]
fn test_config_builder() {
let config = ConfigBuilder::new()
.type_introspection(TypeIntrospectionMethod::MypyDaemon)
.write_changes(true)
.verbosity(2)
.build()
.unwrap();
assert_eq!(
config.type_introspection,
TypeIntrospectionMethod::MypyDaemon
);
assert!(config.write_changes);
assert_eq!(config.output.verbosity, 2);
}
#[test]
fn test_config_file_operations() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("test_config.json");
let config = ConfigBuilder::new()
.verbosity(3)
.write_changes(true)
.build()
.unwrap();
config.save_to_file(&config_path).unwrap();
let loaded_config = Config::from_file(&config_path).unwrap();
assert_eq!(loaded_config.output.verbosity, 3);
assert!(loaded_config.write_changes);
}
#[test]
fn test_config_validation() {
let mut config = Config::default();
config.timeout.lsp_timeout = 0;
assert!(config.validate().is_err());
}
}