use super::Config;
use tracing::debug;
pub struct ConfigMerger;
#[derive(Debug, Clone)]
pub struct MergeDecision {
pub key: String,
pub source: String,
pub value: String,
}
impl ConfigMerger {
pub fn merge(
defaults: Config,
global: Option<Config>,
project: Option<Config>,
env_overrides: Option<Config>,
) -> (Config, Vec<MergeDecision>) {
let mut decisions = Vec::new();
let mut result = defaults;
if let Some(global_config) = global {
Self::merge_into(&mut result, &global_config, "global", &mut decisions);
}
if let Some(project_config) = project {
Self::merge_into(&mut result, &project_config, "project", &mut decisions);
}
if let Some(env_config) = env_overrides {
Self::merge_into(&mut result, &env_config, "environment", &mut decisions);
}
for decision in &decisions {
debug!(
key = %decision.key,
source = %decision.source,
value = %decision.value,
"Configuration merged"
);
}
(result, decisions)
}
fn merge_into(
target: &mut Config,
source: &Config,
source_name: &str,
decisions: &mut Vec<MergeDecision>,
) {
if let Some(ref provider) = source.providers.default_provider {
if target.providers.default_provider != source.providers.default_provider {
decisions.push(MergeDecision {
key: "providers.default_provider".to_string(),
source: source_name.to_string(),
value: provider.clone(),
});
target.providers.default_provider = Some(provider.clone());
}
}
for (key, value) in &source.providers.api_keys {
if !target.providers.api_keys.contains_key(key) {
decisions.push(MergeDecision {
key: format!("providers.api_keys.{}", key),
source: source_name.to_string(),
value: value.clone(),
});
}
target.providers.api_keys.insert(key.clone(), value.clone());
}
for (key, value) in &source.providers.endpoints {
if !target.providers.endpoints.contains_key(key) {
decisions.push(MergeDecision {
key: format!("providers.endpoints.{}", key),
source: source_name.to_string(),
value: value.clone(),
});
}
target
.providers
.endpoints
.insert(key.clone(), value.clone());
}
if let Some(ref model) = source.defaults.model {
if target.defaults.model != source.defaults.model {
decisions.push(MergeDecision {
key: "defaults.model".to_string(),
source: source_name.to_string(),
value: model.clone(),
});
target.defaults.model = Some(model.clone());
}
}
if let Some(temp) = source.defaults.temperature {
if target.defaults.temperature != source.defaults.temperature {
decisions.push(MergeDecision {
key: "defaults.temperature".to_string(),
source: source_name.to_string(),
value: temp.to_string(),
});
target.defaults.temperature = Some(temp);
}
}
if let Some(tokens) = source.defaults.max_tokens {
if target.defaults.max_tokens != source.defaults.max_tokens {
decisions.push(MergeDecision {
key: "defaults.max_tokens".to_string(),
source: source_name.to_string(),
value: tokens.to_string(),
});
target.defaults.max_tokens = Some(tokens);
}
}
for rule in &source.steering {
if !target.steering.iter().any(|r| r.name == rule.name) {
decisions.push(MergeDecision {
key: format!("steering.{}", rule.name),
source: source_name.to_string(),
value: format!("{} bytes", rule.content.len()),
});
target.steering.push(rule.clone());
}
}
for (key, value) in &source.custom {
if !target.custom.contains_key(key) {
decisions.push(MergeDecision {
key: key.clone(),
source: source_name.to_string(),
value: value.to_string(),
});
}
target.custom.insert(key.clone(), value.clone());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_merge_global_into_defaults() {
let defaults = Config::default();
let mut global = Config::default();
global.defaults.model = Some("gpt-4".to_string());
let (result, decisions) = ConfigMerger::merge(defaults, Some(global), None, None);
assert_eq!(result.defaults.model, Some("gpt-4".to_string()));
assert_eq!(decisions.len(), 1);
assert_eq!(decisions[0].source, "global");
}
#[test]
fn test_merge_project_overrides_global() {
let defaults = Config::default();
let mut global = Config::default();
global.defaults.model = Some("gpt-4".to_string());
let mut project = Config::default();
project.defaults.model = Some("gpt-3.5".to_string());
let (result, decisions) = ConfigMerger::merge(defaults, Some(global), Some(project), None);
assert_eq!(result.defaults.model, Some("gpt-3.5".to_string()));
assert!(decisions.iter().any(|d| d.source == "project"));
}
#[test]
fn test_merge_env_overrides_all() {
let defaults = Config::default();
let mut global = Config::default();
global.defaults.model = Some("gpt-4".to_string());
let mut env = Config::default();
env.defaults.model = Some("gpt-3.5-turbo".to_string());
let (result, decisions) = ConfigMerger::merge(defaults, Some(global), None, Some(env));
assert_eq!(result.defaults.model, Some("gpt-3.5-turbo".to_string()));
assert!(decisions.iter().any(|d| d.source == "environment"));
}
#[test]
fn test_merge_api_keys() {
let defaults = Config::default();
let mut global = Config::default();
global
.providers
.api_keys
.insert("openai".to_string(), "key1".to_string());
let mut project = Config::default();
project
.providers
.api_keys
.insert("anthropic".to_string(), "key2".to_string());
let (result, _) = ConfigMerger::merge(defaults, Some(global), Some(project), None);
assert_eq!(
result.providers.api_keys.get("openai"),
Some(&"key1".to_string())
);
assert_eq!(
result.providers.api_keys.get("anthropic"),
Some(&"key2".to_string())
);
}
#[test]
fn test_merge_decisions_logged() {
let defaults = Config::default();
let mut global = Config::default();
global.defaults.model = Some("gpt-4".to_string());
global.defaults.temperature = Some(0.7);
let (_, decisions) = ConfigMerger::merge(defaults, Some(global), None, None);
assert_eq!(decisions.len(), 2);
assert!(decisions.iter().any(|d| d.key == "defaults.model"));
assert!(decisions.iter().any(|d| d.key == "defaults.temperature"));
}
#[test]
fn test_merge_no_duplicate_decisions() {
let defaults = Config::default();
let mut global = Config::default();
global.defaults.model = Some("gpt-4".to_string());
let mut project = Config::default();
project.defaults.model = Some("gpt-4".to_string());
let (_, decisions) = ConfigMerger::merge(defaults, Some(global), Some(project), None);
let model_decisions: Vec<_> = decisions
.iter()
.filter(|d| d.key == "defaults.model")
.collect();
assert_eq!(model_decisions.len(), 1);
}
}