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::{AnalysisValidation, run_validation, validation_failure, validation_success};
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 parse_runtime_config(contents: &str) -> Result<DebtmapConfig, String> {
let config = toml::from_str::<DebtmapConfig>(contents)
.map_err(|error| format!("Failed to parse .debtmap.toml: {error}"))?;
super::validation::validate_config_result(&config).map_err(|error| error.to_string())?;
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(crate) fn config_search_paths(
explicit: Option<PathBuf>,
current: Option<PathBuf>,
) -> Vec<PathBuf> {
const MAX_TRAVERSAL_DEPTH: usize = 10;
match explicit {
Some(path) => vec![path],
None => current
.into_iter()
.flat_map(|path| directory_ancestors_impl(path, MAX_TRAVERSAL_DEPTH))
.map(|directory| directory.join(".debtmap.toml"))
.collect(),
}
}
pub fn load_config() -> DebtmapConfig {
let explicit = std::env::var_os("DEBTMAP_CONFIG").map(PathBuf::from);
let current = (explicit.is_none())
.then(std::env::current_dir)
.transpose()
.unwrap_or_else(|error| {
log::warn!("Failed to get current directory: {error}. Using defaults.");
None
});
config_search_paths(explicit, current)
.into_iter()
.find_map(|path| try_load_config_from_path(&path))
.unwrap_or_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))
}
#[cfg(test)]
mod config_search_path_tests {
use super::*;
#[test]
fn explicit_path_is_the_only_candidate() {
let paths = config_search_paths(
Some(PathBuf::from("custom/debtmap.toml")),
Some(PathBuf::from("project/src")),
);
assert_eq!(paths, vec![PathBuf::from("custom/debtmap.toml")]);
}
#[test]
fn discovery_candidates_start_at_the_current_directory() {
let paths = config_search_paths(None, Some(PathBuf::from("project/src")));
assert_eq!(paths[0], PathBuf::from("project/src/.debtmap.toml"));
assert_eq!(paths[1], PathBuf::from("project/.debtmap.toml"));
}
}