use crate::error::NoloError;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
const DEFAULT_TODO_KEYWORDS: &[&str] = &["TODO", "FIXME", "HACK", "NOTE", "BUG"];
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub default_path: Option<String>,
pub keywords: Option<Vec<String>>,
pub ignore_patterns: Option<Vec<String>>,
pub report: Option<ReportConfig>,
pub languages: Option<std::collections::HashMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReportConfig {
pub default_format: Option<String>,
pub default_limit: Option<usize>,
}
impl Default for Config {
fn default() -> Self {
Self {
default_path: Some(".".to_string()),
keywords: Some(
DEFAULT_TODO_KEYWORDS
.iter()
.map(|s| s.to_string())
.collect(),
),
ignore_patterns: None,
report: Some(ReportConfig {
default_format: Some("table".to_string()),
default_limit: Some(5),
}),
languages: None,
}
}
}
impl Config {
pub fn load(config_path: Option<&Path>) -> Result<Self, NoloError> {
if let Some(path) = config_path {
return Self::load_from_path(path);
}
let search_paths = Self::get_config_search_paths();
for path in search_paths {
if path.exists() {
return Self::load_from_path(&path);
}
}
tracing::debug!("No config file found, using defaults");
Ok(Self::default())
}
fn load_from_path(path: &Path) -> Result<Self, NoloError> {
let content = fs::read_to_string(path).map_err(|e| {
NoloError::ConfigError(format!("Failed to read config file {:?}: {}", path, e))
})?;
let config: Config = toml::from_str(&content).map_err(|e| {
NoloError::ConfigError(format!("Failed to parse config file {:?}: {}", path, e))
})?;
Ok(config)
}
fn get_config_search_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
paths.push(PathBuf::from("nolo.toml"));
paths.push(PathBuf::from(".nolo.toml"));
if let Some(git_root) = crate::discover::git_root() {
let git_config = PathBuf::from(&git_root).join("nolo.toml");
let git_config_hidden = PathBuf::from(&git_root).join(".nolo.toml");
if git_config != PathBuf::from("nolo.toml") {
paths.push(git_config);
}
if git_config_hidden != PathBuf::from(".nolo.toml") {
paths.push(git_config_hidden);
}
}
if let Some(config_dir) = dirs::config_dir() {
paths.push(config_dir.join("nolo").join("config.toml"));
}
if let Some(home_dir) = dirs::home_dir() {
paths.push(home_dir.join(".nolo.toml"));
}
paths
}
pub fn get_keywords(&self) -> Vec<String> {
self.keywords.clone().unwrap_or_else(|| {
DEFAULT_TODO_KEYWORDS
.iter()
.map(|s| s.to_string())
.collect()
})
}
pub fn get_default_path(&self) -> String {
self.default_path.clone().unwrap_or_else(|| ".".to_string())
}
pub fn get_report_format(&self) -> String {
self.report
.as_ref()
.and_then(|r| r.default_format.clone())
.unwrap_or_else(|| "table".to_string())
}
pub fn get_report_limit(&self) -> usize {
self.report
.as_ref()
.and_then(|r| r.default_limit)
.unwrap_or(5)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = Config::default();
assert_eq!(config.get_default_path(), ".");
assert!(config.get_keywords().contains(&"TODO".to_string()));
assert_eq!(config.get_report_format(), "table");
assert_eq!(config.get_report_limit(), 5);
}
#[test]
fn test_config_parsing() {
let toml_content = r#"
default_path = "/custom/path"
keywords = ["TODO", "CUSTOM"]
[report]
default_format = "json"
default_limit = 10
"#;
let config: Config = toml::from_str(toml_content).unwrap();
assert_eq!(config.get_default_path(), "/custom/path");
assert_eq!(config.get_keywords(), vec!["TODO", "CUSTOM"]);
assert_eq!(config.get_report_format(), "json");
assert_eq!(config.get_report_limit(), 10);
}
#[test]
fn test_load_nonexistent_config() {
let result = Config::load(Some(Path::new("nonexistent.toml")));
assert!(result.is_err());
}
}