ricecoder_storage/config/
merge.rs1use super::Config;
7use tracing::debug;
8
9pub struct ConfigMerger;
11
12#[derive(Debug, Clone)]
14pub struct MergeDecision {
15 pub key: String,
17 pub source: String,
19 pub value: String,
21}
22
23impl ConfigMerger {
24 pub fn merge(
30 defaults: Config,
31 global: Option<Config>,
32 project: Option<Config>,
33 env_overrides: Option<Config>,
34 ) -> (Config, Vec<MergeDecision>) {
35 let mut decisions = Vec::new();
36 let mut result = defaults;
37
38 if let Some(global_config) = global {
40 Self::merge_into(&mut result, &global_config, "global", &mut decisions);
41 }
42
43 if let Some(project_config) = project {
45 Self::merge_into(&mut result, &project_config, "project", &mut decisions);
46 }
47
48 if let Some(env_config) = env_overrides {
50 Self::merge_into(&mut result, &env_config, "environment", &mut decisions);
51 }
52
53 for decision in &decisions {
55 debug!(
56 key = %decision.key,
57 source = %decision.source,
58 value = %decision.value,
59 "Configuration merged"
60 );
61 }
62
63 (result, decisions)
64 }
65
66 fn merge_into(
68 target: &mut Config,
69 source: &Config,
70 source_name: &str,
71 decisions: &mut Vec<MergeDecision>,
72 ) {
73 if let Some(ref provider) = source.providers.default_provider {
75 if target.providers.default_provider != source.providers.default_provider {
76 decisions.push(MergeDecision {
77 key: "providers.default_provider".to_string(),
78 source: source_name.to_string(),
79 value: provider.clone(),
80 });
81 target.providers.default_provider = Some(provider.clone());
82 }
83 }
84
85 for (key, value) in &source.providers.api_keys {
86 if !target.providers.api_keys.contains_key(key) {
87 decisions.push(MergeDecision {
88 key: format!("providers.api_keys.{}", key),
89 source: source_name.to_string(),
90 value: value.clone(),
91 });
92 }
93 target.providers.api_keys.insert(key.clone(), value.clone());
94 }
95
96 for (key, value) in &source.providers.endpoints {
97 if !target.providers.endpoints.contains_key(key) {
98 decisions.push(MergeDecision {
99 key: format!("providers.endpoints.{}", key),
100 source: source_name.to_string(),
101 value: value.clone(),
102 });
103 }
104 target
105 .providers
106 .endpoints
107 .insert(key.clone(), value.clone());
108 }
109
110 if let Some(ref model) = source.defaults.model {
112 if target.defaults.model != source.defaults.model {
113 decisions.push(MergeDecision {
114 key: "defaults.model".to_string(),
115 source: source_name.to_string(),
116 value: model.clone(),
117 });
118 target.defaults.model = Some(model.clone());
119 }
120 }
121
122 if let Some(temp) = source.defaults.temperature {
123 if target.defaults.temperature != source.defaults.temperature {
124 decisions.push(MergeDecision {
125 key: "defaults.temperature".to_string(),
126 source: source_name.to_string(),
127 value: temp.to_string(),
128 });
129 target.defaults.temperature = Some(temp);
130 }
131 }
132
133 if let Some(tokens) = source.defaults.max_tokens {
134 if target.defaults.max_tokens != source.defaults.max_tokens {
135 decisions.push(MergeDecision {
136 key: "defaults.max_tokens".to_string(),
137 source: source_name.to_string(),
138 value: tokens.to_string(),
139 });
140 target.defaults.max_tokens = Some(tokens);
141 }
142 }
143
144 for rule in &source.steering {
146 if !target.steering.iter().any(|r| r.name == rule.name) {
147 decisions.push(MergeDecision {
148 key: format!("steering.{}", rule.name),
149 source: source_name.to_string(),
150 value: format!("{} bytes", rule.content.len()),
151 });
152 target.steering.push(rule.clone());
153 }
154 }
155
156 for (key, value) in &source.custom {
158 if !target.custom.contains_key(key) {
159 decisions.push(MergeDecision {
160 key: key.clone(),
161 source: source_name.to_string(),
162 value: value.to_string(),
163 });
164 }
165 target.custom.insert(key.clone(), value.clone());
166 }
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn test_merge_global_into_defaults() {
176 let defaults = Config::default();
177 let mut global = Config::default();
178 global.defaults.model = Some("gpt-4".to_string());
179
180 let (result, decisions) = ConfigMerger::merge(defaults, Some(global), None, None);
181
182 assert_eq!(result.defaults.model, Some("gpt-4".to_string()));
183 assert_eq!(decisions.len(), 1);
184 assert_eq!(decisions[0].source, "global");
185 }
186
187 #[test]
188 fn test_merge_project_overrides_global() {
189 let defaults = Config::default();
190 let mut global = Config::default();
191 global.defaults.model = Some("gpt-4".to_string());
192
193 let mut project = Config::default();
194 project.defaults.model = Some("gpt-3.5".to_string());
195
196 let (result, decisions) = ConfigMerger::merge(defaults, Some(global), Some(project), None);
197
198 assert_eq!(result.defaults.model, Some("gpt-3.5".to_string()));
199 assert!(decisions.iter().any(|d| d.source == "project"));
201 }
202
203 #[test]
204 fn test_merge_env_overrides_all() {
205 let defaults = Config::default();
206 let mut global = Config::default();
207 global.defaults.model = Some("gpt-4".to_string());
208
209 let mut env = Config::default();
210 env.defaults.model = Some("gpt-3.5-turbo".to_string());
211
212 let (result, decisions) = ConfigMerger::merge(defaults, Some(global), None, Some(env));
213
214 assert_eq!(result.defaults.model, Some("gpt-3.5-turbo".to_string()));
215 assert!(decisions.iter().any(|d| d.source == "environment"));
216 }
217
218 #[test]
219 fn test_merge_api_keys() {
220 let defaults = Config::default();
221 let mut global = Config::default();
222 global
223 .providers
224 .api_keys
225 .insert("openai".to_string(), "key1".to_string());
226
227 let mut project = Config::default();
228 project
229 .providers
230 .api_keys
231 .insert("anthropic".to_string(), "key2".to_string());
232
233 let (result, _) = ConfigMerger::merge(defaults, Some(global), Some(project), None);
234
235 assert_eq!(
236 result.providers.api_keys.get("openai"),
237 Some(&"key1".to_string())
238 );
239 assert_eq!(
240 result.providers.api_keys.get("anthropic"),
241 Some(&"key2".to_string())
242 );
243 }
244
245 #[test]
246 fn test_merge_decisions_logged() {
247 let defaults = Config::default();
248 let mut global = Config::default();
249 global.defaults.model = Some("gpt-4".to_string());
250 global.defaults.temperature = Some(0.7);
251
252 let (_, decisions) = ConfigMerger::merge(defaults, Some(global), None, None);
253
254 assert_eq!(decisions.len(), 2);
255 assert!(decisions.iter().any(|d| d.key == "defaults.model"));
256 assert!(decisions.iter().any(|d| d.key == "defaults.temperature"));
257 }
258
259 #[test]
260 fn test_merge_no_duplicate_decisions() {
261 let defaults = Config::default();
262 let mut global = Config::default();
263 global.defaults.model = Some("gpt-4".to_string());
264
265 let mut project = Config::default();
266 project.defaults.model = Some("gpt-4".to_string()); let (_, decisions) = ConfigMerger::merge(defaults, Some(global), Some(project), None);
269
270 let model_decisions: Vec<_> = decisions
272 .iter()
273 .filter(|d| d.key == "defaults.model")
274 .collect();
275 assert_eq!(model_decisions.len(), 1);
276 }
277}