use std::path::{Path, PathBuf};
use thiserror::Error;
use super::merge::Merge;
use super::types::Config;
use super::validator::ConfigValidator;
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("Failed to read config file: {0}")]
Io(#[from] std::io::Error),
#[error("Failed to parse config: {0}")]
Parse(#[from] toml::de::Error),
#[error("Config file not found: {0}")]
NotFound(PathBuf),
#[error("Invalid configuration: {0}")]
Invalid(String),
#[error("{0}")]
Validation(#[from] super::types::ConfigValidationError),
}
#[derive(Debug)]
pub struct ConfigLoader {
files: Vec<PathBuf>,
validate: bool,
validator: Option<ConfigValidator>,
env_enabled: bool,
}
impl Default for ConfigLoader {
fn default() -> Self {
Self::new()
}
}
impl ConfigLoader {
pub fn new() -> Self {
Self {
files: Vec::new(),
validate: false,
validator: None,
env_enabled: true,
}
}
pub fn file<P: AsRef<Path>>(mut self, path: P) -> Self {
self.files.push(path.as_ref().to_path_buf());
self
}
pub fn files<I, P>(mut self, paths: I) -> Self
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
{
self.files
.extend(paths.into_iter().map(|p| p.as_ref().to_path_buf()));
self
}
pub fn with_validation(mut self, validate: bool) -> Self {
self.validate = validate;
self
}
pub fn with_validator(mut self, validator: ConfigValidator) -> Self {
self.validator = Some(validator);
self
}
pub fn with_env(mut self, enabled: bool) -> Self {
self.env_enabled = enabled;
self
}
fn apply_env_overrides(&self, config: &mut Config) {
if !self.env_enabled {
return;
}
if let Ok(api_key) = std::env::var("OPENAI_API_KEY") {
config.llm.api_key = Some(api_key.clone());
if config.llm.summary.api_key.is_none() {
config.llm.summary.api_key = Some(api_key.clone());
}
if config.llm.retrieval.api_key.is_none() {
config.llm.retrieval.api_key = Some(api_key.clone());
}
if config.llm.pilot.api_key.is_none() {
config.llm.pilot.api_key = Some(api_key);
}
}
if let Ok(model) = std::env::var("VECTORLESS_MODEL") {
config.llm.summary.model = model.clone();
config.llm.retrieval.model = model.clone();
config.llm.pilot.model = model;
}
if let Ok(endpoint) = std::env::var("VECTORLESS_ENDPOINT") {
config.llm.summary.endpoint = endpoint.clone();
config.llm.retrieval.endpoint = endpoint.clone();
config.llm.pilot.endpoint = endpoint;
}
if let Ok(workspace) = std::env::var("VECTORLESS_WORKSPACE") {
config.storage.workspace_dir = PathBuf::from(workspace);
}
}
pub fn load(self) -> Result<Config, ConfigError> {
let mut config = Config::default();
for path in &self.files {
if path.exists() {
let content = std::fs::read_to_string(path)?;
let file_config: Config = toml::from_str(&content)?;
config.merge(&file_config, super::merge::MergeStrategy::Replace);
} else {
return Err(ConfigError::NotFound(path.clone()));
}
}
self.apply_env_overrides(&mut config);
if self.validate {
let validator = self.validator.unwrap_or_default();
validator.validate(&config)?;
}
Ok(config)
}
}
pub const CONFIG_FILE_NAMES: &[&str] = &["vectorless.toml", "config.toml", ".vectorless.toml"];
pub fn find_config_file() -> Option<PathBuf> {
let current_dir = std::env::current_dir().ok()?;
for name in CONFIG_FILE_NAMES {
let path = current_dir.join(name);
if path.exists() {
return Some(path);
}
}
let mut dir = current_dir.as_path();
for _ in 0..3 {
if let Some(parent) = dir.parent() {
for name in CONFIG_FILE_NAMES {
let path = parent.join(name);
if path.exists() {
return Some(path);
}
}
dir = parent;
} else {
break;
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = Config::default();
assert_eq!(config.indexer.subsection_threshold, 300);
assert_eq!(config.summary.model, "gpt-4o-mini");
assert_eq!(config.retrieval.model, "gpt-4o");
}
#[test]
fn test_config_loader_defaults() {
let config = ConfigLoader::new().load().unwrap();
assert_eq!(config.indexer.subsection_threshold, 300);
}
#[test]
fn test_config_loader_not_found() {
let result = ConfigLoader::new().file("nonexistent_config.toml").load();
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), ConfigError::NotFound(_)));
}
#[test]
fn test_config_loader_with_validation() {
let config = ConfigLoader::new().with_validation(true).load().unwrap();
assert_eq!(config.retrieval.model, "gpt-4o");
}
}