1use std::fs;
2use std::io::{BufReader, Read};
3use std::path::{Path, PathBuf};
4
5use super::core::DebtmapConfig;
6use super::scoring::ScoringWeights;
7use super::validation::validate_config;
8use crate::effects::{run_validation, validation_failure, validation_success, AnalysisValidation};
9use crate::errors::AnalysisError;
10
11pub(crate) fn read_config_file(path: &Path) -> Result<String, std::io::Error> {
14 let file = fs::File::open(path)?;
15 let mut reader = BufReader::new(file);
16 let mut contents = String::new();
17 reader.read_to_string(&mut contents)?;
18 Ok(contents)
19}
20
21pub fn parse_and_validate_config(contents: &str) -> Result<DebtmapConfig, String> {
23 parse_and_validate_config_impl(contents)
24}
25
26pub(crate) fn parse_and_validate_config_impl(contents: &str) -> Result<DebtmapConfig, String> {
27 let mut config = toml::from_str::<DebtmapConfig>(contents)
28 .map_err(|e| format!("Failed to parse .debtmap.toml: {}", e))?;
29
30 if let Some(ref mut scoring) = config.scoring {
32 if let Err(e) = scoring.validate() {
33 eprintln!("Warning: Invalid scoring weights: {}. Using defaults.", e);
34 config.scoring = Some(ScoringWeights::default());
35 } else {
36 scoring.normalize(); }
38 }
39
40 Ok(config)
41}
42
43pub(crate) fn try_load_config_from_path(config_path: &Path) -> Option<DebtmapConfig> {
45 let contents = match read_config_file(config_path) {
46 Ok(contents) => contents,
47 Err(e) => {
48 handle_read_error(config_path, &e);
49 return None;
50 }
51 };
52
53 match parse_and_validate_config_impl(&contents) {
54 Ok(config) => {
55 log::debug!("Loaded config from {}", config_path.display());
56 Some(config)
57 }
58 Err(e) => {
59 eprintln!("Warning: {}. Using defaults.", e);
60 None
61 }
62 }
63}
64
65pub(crate) fn handle_read_error(config_path: &Path, error: &std::io::Error) {
67 if error.kind() != std::io::ErrorKind::NotFound {
69 log::warn!(
70 "Failed to read config file {}: {}",
71 config_path.display(),
72 error
73 );
74 }
75}
76
77pub fn directory_ancestors(start: PathBuf, max_depth: usize) -> impl Iterator<Item = PathBuf> {
79 directory_ancestors_impl(start, max_depth)
80}
81
82pub(crate) fn directory_ancestors_impl(
83 start: PathBuf,
84 max_depth: usize,
85) -> impl Iterator<Item = PathBuf> {
86 std::iter::successors(Some(start), |dir| {
87 let mut parent = dir.clone();
88 if parent.pop() {
89 Some(parent)
90 } else {
91 None
92 }
93 })
94 .take(max_depth)
95}
96
97pub fn load_config() -> DebtmapConfig {
98 const MAX_TRAVERSAL_DEPTH: usize = 10;
99
100 let current = match std::env::current_dir() {
102 Ok(dir) => dir,
103 Err(e) => {
104 log::warn!(
105 "Failed to get current directory: {}. Using default config.",
106 e
107 );
108 return DebtmapConfig::default();
109 }
110 };
111
112 directory_ancestors_impl(current, MAX_TRAVERSAL_DEPTH)
114 .map(|dir| dir.join(".debtmap.toml"))
115 .find_map(|path| try_load_config_from_path(&path))
116 .unwrap_or_else(|| {
117 log::debug!(
118 "No config found after checking {} directories. Using default config.",
119 MAX_TRAVERSAL_DEPTH
120 );
121 DebtmapConfig::default()
122 })
123}
124
125pub fn load_config_validated(start_dir: &Path) -> AnalysisValidation<DebtmapConfig> {
156 const MAX_TRAVERSAL_DEPTH: usize = 10;
157
158 let config_path = directory_ancestors_impl(start_dir.to_path_buf(), MAX_TRAVERSAL_DEPTH)
160 .map(|dir| dir.join(".debtmap.toml"))
161 .find(|path| path.exists());
162
163 let Some(config_path) = config_path else {
164 return validation_success(DebtmapConfig::default());
166 };
167
168 let contents = match read_config_file(&config_path) {
170 Ok(contents) => contents,
171 Err(e) => {
172 return validation_failure(AnalysisError::io_with_path(
173 format!("Cannot read config file: {}", e),
174 &config_path,
175 ));
176 }
177 };
178
179 let config = match toml::from_str::<DebtmapConfig>(&contents) {
181 Ok(config) => config,
182 Err(e) => {
183 return validation_failure(AnalysisError::config_with_path(
184 format!("Failed to parse config: {}", e),
185 &config_path,
186 ));
187 }
188 };
189
190 match validate_config(&config) {
192 stillwater::Validation::Success(_) => validation_success(config),
193 stillwater::Validation::Failure(errors) => stillwater::Validation::Failure(errors),
194 }
195}
196
197pub fn load_config_validated_result(start_dir: &Path) -> anyhow::Result<DebtmapConfig> {
202 run_validation(load_config_validated(start_dir))
203}
204
205pub fn load_config_from_path_validated(config_path: &Path) -> AnalysisValidation<DebtmapConfig> {
210 if !config_path.exists() {
212 return validation_failure(AnalysisError::config_with_path(
213 format!("Config file not found: {}", config_path.display()),
214 config_path,
215 ));
216 }
217
218 let contents = match read_config_file(config_path) {
220 Ok(contents) => contents,
221 Err(e) => {
222 return validation_failure(AnalysisError::io_with_path(
223 format!("Cannot read config file: {}", e),
224 config_path,
225 ));
226 }
227 };
228
229 let config = match toml::from_str::<DebtmapConfig>(&contents) {
231 Ok(config) => config,
232 Err(e) => {
233 return validation_failure(AnalysisError::config_with_path(
234 format!("Failed to parse config: {}", e),
235 config_path,
236 ));
237 }
238 };
239
240 match validate_config(&config) {
242 stillwater::Validation::Success(_) => validation_success(config),
243 stillwater::Validation::Failure(errors) => stillwater::Validation::Failure(errors),
244 }
245}
246
247pub fn load_config_from_path_result(config_path: &Path) -> anyhow::Result<DebtmapConfig> {
249 run_validation(load_config_from_path_validated(config_path))
250}