1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::config::bootstrap::{
4 ResolutionFrame, explain_default_profile_bootstrap, explain_default_profile_key,
5 prepare_resolution,
6};
7use crate::config::explain::{
8 build_runtime_explain, explain_layers_for_runtime_key, selected_value,
9};
10use crate::config::interpolate::{explain_interpolation, interpolate_all};
11use crate::config::selector::{LayerRef, ScopeSelector, SelectedLayerEntry};
12use crate::config::{
13 BootstrapConfigExplain, ConfigError, ConfigExplain, ConfigLayer, ConfigSchema, ConfigSource,
14 ConfigValue, LoadedLayers, ResolveOptions, ResolvedConfig, ResolvedValue, Scope, is_alias_key,
15 is_bootstrap_only_key,
16};
17
18#[derive(Debug, Clone, Default)]
19pub struct ConfigResolver {
20 layers: LoadedLayers,
21 schema: ConfigSchema,
22}
23
24#[derive(Debug, Clone)]
28struct ResolvedMaps {
29 pre_interpolated: BTreeMap<String, ResolvedValue>,
30 final_values: BTreeMap<String, ResolvedValue>,
31 alias_values: BTreeMap<String, ResolvedValue>,
32}
33
34impl ConfigResolver {
35 pub fn from_loaded_layers(layers: LoadedLayers) -> Self {
36 Self {
37 layers,
38 schema: ConfigSchema::default(),
39 }
40 }
41
42 pub fn set_schema(&mut self, schema: ConfigSchema) {
43 self.schema = schema;
44 }
45
46 pub fn schema_mut(&mut self) -> &mut ConfigSchema {
47 &mut self.schema
48 }
49
50 pub fn defaults_mut(&mut self) -> &mut ConfigLayer {
51 &mut self.layers.defaults
52 }
53
54 pub fn file_mut(&mut self) -> &mut ConfigLayer {
55 &mut self.layers.file
56 }
57
58 pub fn presentation_mut(&mut self) -> &mut ConfigLayer {
59 &mut self.layers.presentation
60 }
61
62 pub fn secrets_mut(&mut self) -> &mut ConfigLayer {
63 &mut self.layers.secrets
64 }
65
66 pub fn env_mut(&mut self) -> &mut ConfigLayer {
67 &mut self.layers.env
68 }
69
70 pub fn cli_mut(&mut self) -> &mut ConfigLayer {
71 &mut self.layers.cli
72 }
73
74 pub fn session_mut(&mut self) -> &mut ConfigLayer {
75 &mut self.layers.session
76 }
77
78 pub fn set_defaults(&mut self, layer: ConfigLayer) {
79 self.layers.defaults = layer;
80 }
81
82 pub fn set_file(&mut self, layer: ConfigLayer) {
83 self.layers.file = layer;
84 }
85
86 pub fn set_presentation(&mut self, layer: ConfigLayer) {
87 self.layers.presentation = layer;
88 }
89
90 pub fn set_secrets(&mut self, layer: ConfigLayer) {
91 self.layers.secrets = layer;
92 }
93
94 pub fn set_env(&mut self, layer: ConfigLayer) {
95 self.layers.env = layer;
96 }
97
98 pub fn set_cli(&mut self, layer: ConfigLayer) {
99 self.layers.cli = layer;
100 }
101
102 pub fn set_session(&mut self, layer: ConfigLayer) {
103 self.layers.session = layer;
104 }
105
106 pub fn resolve(&self, options: ResolveOptions) -> Result<ResolvedConfig, ConfigError> {
107 tracing::debug!(
108 profile_override = ?options.profile_override,
109 terminal = ?options.terminal,
110 "resolving config"
111 );
112 let frame = prepare_resolution(self.layers(), options)?;
113 let resolved = self.resolve_maps_for_frame(&frame)?;
114 let config = ResolvedConfig {
115 active_profile: frame.active_profile,
116 terminal: frame.terminal,
117 known_profiles: frame.known_profiles,
118 values: resolved.final_values,
119 aliases: resolved.alias_values,
120 };
121 tracing::debug!(
122 active_profile = %config.active_profile(),
123 terminal = ?config.terminal(),
124 values = config.values().len(),
125 aliases = config.aliases().len(),
126 "resolved config"
127 );
128 Ok(config)
129 }
130
131 pub fn explain_key(
132 &self,
133 key: &str,
134 options: ResolveOptions,
135 ) -> Result<ConfigExplain, ConfigError> {
136 if key.eq_ignore_ascii_case("profile.default") {
137 return explain_default_profile_key(self.layers(), options);
138 }
139
140 let frame = prepare_resolution(self.layers(), options)?;
141 let layers = explain_layers_for_runtime_key(self.layers(), key, &frame);
142 let resolved = self.resolve_maps_for_frame(&frame)?;
143 let final_entry = if is_alias_key(key) {
144 resolved.alias_values.get(key).cloned()
145 } else {
146 resolved.final_values.get(key).cloned()
147 };
148 let interpolation =
152 explain_interpolation(key, &resolved.pre_interpolated, &resolved.final_values)?;
153
154 Ok(build_runtime_explain(
155 key,
156 frame,
157 layers,
158 final_entry,
159 if is_alias_key(key) {
160 None
161 } else {
162 interpolation
163 },
164 ))
165 }
166
167 pub fn explain_bootstrap_key(
168 &self,
169 key: &str,
170 options: ResolveOptions,
171 ) -> Result<BootstrapConfigExplain, ConfigError> {
172 if key.eq_ignore_ascii_case("profile.default") {
173 return explain_default_profile_bootstrap(self.layers(), options);
174 }
175
176 Err(ConfigError::InvalidConfigKey {
177 key: key.to_string(),
178 reason: "not a bootstrap key".to_string(),
179 })
180 }
181
182 fn resolve_maps_for_frame(&self, frame: &ResolutionFrame) -> Result<ResolvedMaps, ConfigError> {
186 tracing::trace!(
187 active_profile = %frame.active_profile,
188 terminal = ?frame.terminal,
189 "resolving config maps for frame"
190 );
191 let mut pre_interpolated = self.collect_selected_values_for_frame(frame);
192 let alias_values = Self::drain_alias_values(&mut pre_interpolated);
196 let mut final_values = pre_interpolated.clone();
200 interpolate_all(&mut final_values)?;
201 self.schema.validate_and_adapt(&mut final_values)?;
202
203 tracing::trace!(
204 pre_interpolated = pre_interpolated.len(),
205 final_values = final_values.len(),
206 aliases = alias_values.len(),
207 "resolved config maps for frame"
208 );
209 Ok(ResolvedMaps {
210 pre_interpolated,
211 final_values,
212 alias_values,
213 })
214 }
215
216 fn collect_selected_values_for_frame(
221 &self,
222 frame: &ResolutionFrame,
223 ) -> BTreeMap<String, ResolvedValue> {
224 let selector = ScopeSelector::scoped(&frame.active_profile, frame.terminal.as_deref());
225 let keys = self.collect_keys();
226
227 let mut values = BTreeMap::new();
228 for key in keys {
229 if is_bootstrap_only_key(&key) {
230 continue;
231 }
232 if let Some(selected) = self.select_across_layers(&key, selector) {
233 values.insert(key, selected_value(&selected));
234 }
235 }
236
237 values.insert(
238 "profile.active".to_string(),
239 Self::derived_active_profile_value(frame),
240 );
241
242 values
243 }
244
245 fn derived_active_profile_value(frame: &ResolutionFrame) -> ResolvedValue {
248 ResolvedValue {
249 raw_value: ConfigValue::String(frame.active_profile.to_string()),
250 value: ConfigValue::String(frame.active_profile.to_string()),
251 source: ConfigSource::Derived,
252 scope: Scope::global(),
253 origin: None,
254 }
255 }
256
257 fn collect_keys(&self) -> BTreeSet<String> {
258 let mut keys = BTreeSet::new();
259
260 for layer in self.layers() {
261 for entry in &layer.layer.entries {
262 keys.insert(entry.key.clone());
263 }
264 }
265
266 keys
267 }
268
269 fn drain_alias_values(
270 values: &mut BTreeMap<String, ResolvedValue>,
271 ) -> BTreeMap<String, ResolvedValue> {
272 let alias_keys = values
273 .keys()
274 .filter(|key| is_alias_key(key))
275 .cloned()
276 .collect::<Vec<_>>();
277 let mut aliases = BTreeMap::new();
278 for key in alias_keys {
279 if let Some(value) = values.remove(&key) {
280 aliases.insert(key, value);
281 }
282 }
283 aliases
284 }
285
286 fn select_across_layers<'a>(
287 &'a self,
288 key: &str,
289 selector: ScopeSelector<'a>,
290 ) -> Option<SelectedLayerEntry<'a>> {
291 let mut selected: Option<SelectedLayerEntry<'a>> = None;
292
293 for layer in self.layers() {
296 if let Some(entry) = selector.select(layer, key) {
297 if let Some(previous) = &selected {
298 tracing::trace!(
299 key = %key,
300 previous_source = ?previous.source,
301 next_source = ?entry.source,
302 "config key winner changed across layers"
303 );
304 }
305 selected = Some(entry);
306 }
307 }
308
309 selected
310 }
311
312 fn layers(&self) -> [LayerRef<'_>; 7] {
313 [
316 LayerRef {
317 source: ConfigSource::BuiltinDefaults,
318 layer: &self.layers.defaults,
319 },
320 LayerRef {
321 source: ConfigSource::PresentationDefaults,
322 layer: &self.layers.presentation,
323 },
324 LayerRef {
325 source: ConfigSource::ConfigFile,
326 layer: &self.layers.file,
327 },
328 LayerRef {
329 source: ConfigSource::Secrets,
330 layer: &self.layers.secrets,
331 },
332 LayerRef {
333 source: ConfigSource::Environment,
334 layer: &self.layers.env,
335 },
336 LayerRef {
337 source: ConfigSource::Cli,
338 layer: &self.layers.cli,
339 },
340 LayerRef {
341 source: ConfigSource::Session,
342 layer: &self.layers.session,
343 },
344 ]
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::ConfigResolver;
351 use crate::config::{ConfigError, ConfigLayer, ResolveOptions};
352
353 #[test]
354 fn resolver_layer_mutators_and_setters_are_callable_unit() {
355 let mut resolver = ConfigResolver::default();
356 resolver.defaults_mut().set("profile.default", "default");
357 resolver.file_mut().set("theme.name", "file");
358 resolver.secrets_mut().set("profile.default", "default");
359 resolver.env_mut().set("theme.name", "env");
360 resolver.cli_mut().set("theme.name", "cli");
361 resolver.session_mut().set("theme.name", "session");
362
363 let resolved = resolver
364 .resolve(ResolveOptions::default().with_terminal("cli"))
365 .expect("resolver should resolve");
366 assert_eq!(resolved.get_string("theme.name"), Some("session"));
367 assert_eq!(resolved.active_profile(), "default");
368
369 let mut replacement = ConfigLayer::default();
370 replacement.set("profile.default", "default");
371 replacement.set("theme.name", "replaced");
372 resolver.set_defaults(replacement);
373 resolver.set_file(ConfigLayer::default());
374 resolver.set_secrets(ConfigLayer::default());
375 resolver.set_env(ConfigLayer::default());
376 resolver.set_cli(ConfigLayer::default());
377 resolver.set_session(ConfigLayer::default());
378
379 let replaced = resolver
380 .resolve(ResolveOptions::default().with_terminal("cli"))
381 .expect("replacement config should resolve");
382 assert_eq!(replaced.get_string("theme.name"), Some("replaced"));
383 }
384
385 #[test]
386 fn explain_bootstrap_key_rejects_non_bootstrap_keys_unit() {
387 let resolver = ConfigResolver::default();
388 let err = resolver
389 .explain_bootstrap_key("ui.theme", ResolveOptions::default())
390 .expect_err("non-bootstrap key should fail");
391
392 assert!(matches!(
393 err,
394 ConfigError::InvalidConfigKey { key, .. } if key == "ui.theme"
395 ));
396 }
397}