use std::path::PathBuf;
use anyhow::{Context, Result};
use crate::loader::layers::{ConfigLayerEntry, ConfigLayerSource};
use crate::loader::manager::ConfigManager;
#[derive(Debug, Clone, Default)]
pub struct ConfigBuilder {
workspace: Option<PathBuf>,
config_file: Option<PathBuf>,
cli_overrides: Vec<(String, toml::Value)>,
}
impl ConfigBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn workspace(mut self, path: PathBuf) -> Self {
self.workspace = Some(path);
self
}
pub fn config_file(mut self, path: PathBuf) -> Self {
self.config_file = Some(path);
self
}
pub fn cli_override(mut self, key: String, value: toml::Value) -> Self {
self.cli_overrides.push((key, value));
self
}
pub fn cli_overrides(mut self, overrides: &[(String, String)]) -> Self {
for (key, value) in overrides {
let toml_value = value
.parse::<toml::Value>()
.unwrap_or_else(|_| toml::Value::String(value.clone()));
self.cli_overrides.push((key.clone(), toml_value));
}
self
}
pub fn build(self) -> Result<ConfigManager> {
let workspace = self
.workspace
.clone()
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
let mut manager = if let Some(config_file) = self.config_file {
ConfigManager::load_from_file(config_file)?
} else {
ConfigManager::load_from_workspace(workspace)?
};
if !self.cli_overrides.is_empty() {
let mut runtime_toml = toml::Table::new();
for (key, value) in self.cli_overrides {
Self::insert_dotted_key(&mut runtime_toml, &key, value);
}
let runtime_layer =
ConfigLayerEntry::new(ConfigLayerSource::Runtime, toml::Value::Table(runtime_toml));
manager.layer_stack.push(runtime_layer);
let effective_toml = manager.layer_stack.effective_config();
manager.config = effective_toml
.try_into()
.context("Failed to deserialize effective configuration after runtime overrides")?;
manager
.config
.validate()
.context("Configuration failed validation after runtime overrides")?;
}
Ok(manager)
}
pub(crate) fn insert_dotted_key(table: &mut toml::Table, key: &str, value: toml::Value) {
let parts: Vec<&str> = key.split('.').collect();
let mut current = table;
for (i, part) in parts.iter().enumerate() {
if i == parts.len() - 1 {
current.insert(part.to_string(), value);
return;
}
if !current.contains_key(*part) || !current[*part].is_table() {
current.insert(part.to_string(), toml::Value::Table(toml::Table::new()));
}
current = current
.get_mut(*part)
.and_then(|v| v.as_table_mut())
.unwrap_or_else(|| unreachable!("inserted table should remain a table"));
}
}
}