use std::fs;
use std::io::{BufReader, Read};
use std::path::{Path, PathBuf};
use super::core::DebtmapConfig;
use super::scoring::ScoringWeights;
use super::validation::validate_config;
use crate::effects::{run_validation, validation_failure, validation_success, AnalysisValidation};
use crate::errors::AnalysisError;
pub(crate) fn read_config_file(path: &Path) -> Result<String, std::io::Error> {
let file = fs::File::open(path)?;
let mut reader = BufReader::new(file);
let mut contents = String::new();
reader.read_to_string(&mut contents)?;
Ok(contents)
}
pub fn parse_and_validate_config(contents: &str) -> Result<DebtmapConfig, String> {
parse_and_validate_config_impl(contents)
}
pub(crate) fn parse_and_validate_config_impl(contents: &str) -> Result<DebtmapConfig, String> {
let mut config = toml::from_str::<DebtmapConfig>(contents)
.map_err(|e| format!("Failed to parse .debtmap.toml: {}", e))?;
if let Some(ref mut scoring) = config.scoring {
if let Err(e) = scoring.validate() {
eprintln!("Warning: Invalid scoring weights: {}. Using defaults.", e);
config.scoring = Some(ScoringWeights::default());
} else {
scoring.normalize(); }
}
Ok(config)
}
pub(crate) fn try_load_config_from_path(config_path: &Path) -> Option<DebtmapConfig> {
let contents = match read_config_file(config_path) {
Ok(contents) => contents,
Err(e) => {
handle_read_error(config_path, &e);
return None;
}
};
match parse_and_validate_config_impl(&contents) {
Ok(config) => {
log::debug!("Loaded config from {}", config_path.display());
Some(config)
}
Err(e) => {
eprintln!("Warning: {}. Using defaults.", e);
None
}
}
}
pub(crate) fn handle_read_error(config_path: &Path, error: &std::io::Error) {
if error.kind() != std::io::ErrorKind::NotFound {
log::warn!(
"Failed to read config file {}: {}",
config_path.display(),
error
);
}
}
pub fn directory_ancestors(start: PathBuf, max_depth: usize) -> impl Iterator<Item = PathBuf> {
directory_ancestors_impl(start, max_depth)
}
pub(crate) fn directory_ancestors_impl(
start: PathBuf,
max_depth: usize,
) -> impl Iterator<Item = PathBuf> {
std::iter::successors(Some(start), |dir| {
let mut parent = dir.clone();
if parent.pop() {
Some(parent)
} else {
None
}
})
.take(max_depth)
}
pub fn load_config() -> DebtmapConfig {
const MAX_TRAVERSAL_DEPTH: usize = 10;
let current = match std::env::current_dir() {
Ok(dir) => dir,
Err(e) => {
log::warn!(
"Failed to get current directory: {}. Using default config.",
e
);
return DebtmapConfig::default();
}
};
directory_ancestors_impl(current, MAX_TRAVERSAL_DEPTH)
.map(|dir| dir.join(".debtmap.toml"))
.find_map(|path| try_load_config_from_path(&path))
.unwrap_or_else(|| {
log::debug!(
"No config found after checking {} directories. Using default config.",
MAX_TRAVERSAL_DEPTH
);
DebtmapConfig::default()
})
}
pub fn load_config_validated(start_dir: &Path) -> AnalysisValidation<DebtmapConfig> {
const MAX_TRAVERSAL_DEPTH: usize = 10;
let config_path = directory_ancestors_impl(start_dir.to_path_buf(), MAX_TRAVERSAL_DEPTH)
.map(|dir| dir.join(".debtmap.toml"))
.find(|path| path.exists());
let Some(config_path) = config_path else {
return validation_success(DebtmapConfig::default());
};
let contents = match read_config_file(&config_path) {
Ok(contents) => contents,
Err(e) => {
return validation_failure(AnalysisError::io_with_path(
format!("Cannot read config file: {}", e),
&config_path,
));
}
};
let config = match toml::from_str::<DebtmapConfig>(&contents) {
Ok(config) => config,
Err(e) => {
return validation_failure(AnalysisError::config_with_path(
format!("Failed to parse config: {}", e),
&config_path,
));
}
};
match validate_config(&config) {
stillwater::Validation::Success(_) => validation_success(config),
stillwater::Validation::Failure(errors) => stillwater::Validation::Failure(errors),
}
}
pub fn load_config_validated_result(start_dir: &Path) -> anyhow::Result<DebtmapConfig> {
run_validation(load_config_validated(start_dir))
}
pub fn load_config_from_path_validated(config_path: &Path) -> AnalysisValidation<DebtmapConfig> {
if !config_path.exists() {
return validation_failure(AnalysisError::config_with_path(
format!("Config file not found: {}", config_path.display()),
config_path,
));
}
let contents = match read_config_file(config_path) {
Ok(contents) => contents,
Err(e) => {
return validation_failure(AnalysisError::io_with_path(
format!("Cannot read config file: {}", e),
config_path,
));
}
};
let config = match toml::from_str::<DebtmapConfig>(&contents) {
Ok(config) => config,
Err(e) => {
return validation_failure(AnalysisError::config_with_path(
format!("Failed to parse config: {}", e),
config_path,
));
}
};
match validate_config(&config) {
stillwater::Validation::Success(_) => validation_success(config),
stillwater::Validation::Failure(errors) => stillwater::Validation::Failure(errors),
}
}
pub fn load_config_from_path_result(config_path: &Path) -> anyhow::Result<DebtmapConfig> {
run_validation(load_config_from_path_validated(config_path))
}