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)]
38pub struct ConfigResolver {
39 layers: LoadedLayers,
40 schema: ConfigSchema,
41}
42
43#[derive(Debug, Clone)]
47struct ResolvedMaps {
48 pre_interpolated: BTreeMap<String, ResolvedValue>,
49 final_values: BTreeMap<String, ResolvedValue>,
50 alias_values: BTreeMap<String, ResolvedValue>,
51}
52
53impl ConfigResolver {
54 pub fn from_loaded_layers(layers: LoadedLayers) -> Self {
56 Self {
57 layers,
58 schema: ConfigSchema::default(),
59 }
60 }
61
62 pub fn set_schema(&mut self, schema: ConfigSchema) {
64 self.schema = schema;
65 }
66
67 pub fn schema_mut(&mut self) -> &mut ConfigSchema {
69 &mut self.schema
70 }
71
72 pub fn defaults_mut(&mut self) -> &mut ConfigLayer {
74 &mut self.layers.defaults
75 }
76
77 pub fn file_mut(&mut self) -> &mut ConfigLayer {
79 &mut self.layers.file
80 }
81
82 pub fn presentation_mut(&mut self) -> &mut ConfigLayer {
84 &mut self.layers.presentation
85 }
86
87 pub fn secrets_mut(&mut self) -> &mut ConfigLayer {
89 &mut self.layers.secrets
90 }
91
92 pub fn env_mut(&mut self) -> &mut ConfigLayer {
94 &mut self.layers.env
95 }
96
97 pub fn cli_mut(&mut self) -> &mut ConfigLayer {
99 &mut self.layers.cli
100 }
101
102 pub fn session_mut(&mut self) -> &mut ConfigLayer {
104 &mut self.layers.session
105 }
106
107 pub fn set_defaults(&mut self, layer: ConfigLayer) {
109 self.layers.defaults = layer;
110 }
111
112 pub fn set_file(&mut self, layer: ConfigLayer) {
114 self.layers.file = layer;
115 }
116
117 pub fn set_presentation(&mut self, layer: ConfigLayer) {
119 self.layers.presentation = layer;
120 }
121
122 pub fn set_secrets(&mut self, layer: ConfigLayer) {
124 self.layers.secrets = layer;
125 }
126
127 pub fn set_env(&mut self, layer: ConfigLayer) {
129 self.layers.env = layer;
130 }
131
132 pub fn set_cli(&mut self, layer: ConfigLayer) {
134 self.layers.cli = layer;
135 }
136
137 pub fn set_session(&mut self, layer: ConfigLayer) {
139 self.layers.session = layer;
140 }
141
142 pub fn resolve(&self, options: ResolveOptions) -> Result<ResolvedConfig, ConfigError> {
164 tracing::debug!(
165 profile_override = ?options.profile_override,
166 terminal = ?options.terminal,
167 "resolving config"
168 );
169 let frame = prepare_resolution(self.layers(), options)?;
170 let resolved = self.resolve_maps_for_frame(&frame)?;
171 let config = ResolvedConfig {
172 active_profile: frame.active_profile,
173 terminal: frame.terminal,
174 known_profiles: frame.known_profiles,
175 values: resolved.final_values,
176 aliases: resolved.alias_values,
177 };
178 tracing::debug!(
179 active_profile = %config.active_profile(),
180 terminal = ?config.terminal(),
181 values = config.values().len(),
182 aliases = config.aliases().len(),
183 "resolved config"
184 );
185 Ok(config)
186 }
187
188 pub fn explain_key(
215 &self,
216 key: &str,
217 options: ResolveOptions,
218 ) -> Result<ConfigExplain, ConfigError> {
219 if key.eq_ignore_ascii_case("profile.default") {
220 return explain_default_profile_key(self.layers(), options);
221 }
222
223 let frame = prepare_resolution(self.layers(), options)?;
224 let layers = explain_layers_for_runtime_key(self.layers(), key, &frame);
225 let resolved = self.resolve_maps_for_frame(&frame)?;
226 let final_entry = if is_alias_key(key) {
227 resolved.alias_values.get(key).cloned()
228 } else {
229 resolved.final_values.get(key).cloned()
230 };
231 let interpolation =
235 explain_interpolation(key, &resolved.pre_interpolated, &resolved.final_values)?;
236
237 Ok(build_runtime_explain(
238 key,
239 frame,
240 layers,
241 final_entry,
242 if is_alias_key(key) {
243 None
244 } else {
245 interpolation
246 },
247 ))
248 }
249
250 pub fn explain_bootstrap_key(
252 &self,
253 key: &str,
254 options: ResolveOptions,
255 ) -> Result<BootstrapConfigExplain, ConfigError> {
256 if key.eq_ignore_ascii_case("profile.default") {
257 return explain_default_profile_bootstrap(self.layers(), options);
258 }
259
260 Err(ConfigError::InvalidConfigKey {
261 key: key.to_string(),
262 reason: "not a bootstrap key".to_string(),
263 })
264 }
265
266 fn resolve_maps_for_frame(&self, frame: &ResolutionFrame) -> Result<ResolvedMaps, ConfigError> {
270 tracing::trace!(
271 active_profile = %frame.active_profile,
272 terminal = ?frame.terminal,
273 "resolving config maps for frame"
274 );
275 let mut pre_interpolated = self.collect_selected_values_for_frame(frame);
276 let alias_values = Self::drain_alias_values(&mut pre_interpolated);
280 let mut final_values = pre_interpolated.clone();
284 interpolate_all(&mut final_values)?;
285 self.schema.validate_and_adapt(&mut final_values)?;
286
287 tracing::trace!(
288 pre_interpolated = pre_interpolated.len(),
289 final_values = final_values.len(),
290 aliases = alias_values.len(),
291 "resolved config maps for frame"
292 );
293 Ok(ResolvedMaps {
294 pre_interpolated,
295 final_values,
296 alias_values,
297 })
298 }
299
300 fn collect_selected_values_for_frame(
305 &self,
306 frame: &ResolutionFrame,
307 ) -> BTreeMap<String, ResolvedValue> {
308 let selector = ScopeSelector::scoped(&frame.active_profile, frame.terminal.as_deref());
309 let keys = self.collect_keys();
310
311 let mut values = BTreeMap::new();
312 for key in keys {
313 if is_bootstrap_only_key(&key) {
314 continue;
315 }
316 if let Some(selected) = self.select_across_layers(&key, selector) {
317 values.insert(key, selected_value(&selected));
318 }
319 }
320
321 values.insert(
322 "profile.active".to_string(),
323 Self::derived_active_profile_value(frame),
324 );
325
326 values
327 }
328
329 fn derived_active_profile_value(frame: &ResolutionFrame) -> ResolvedValue {
332 ResolvedValue {
333 raw_value: ConfigValue::String(frame.active_profile.to_string()),
334 value: ConfigValue::String(frame.active_profile.to_string()),
335 source: ConfigSource::Derived,
336 scope: Scope::global(),
337 origin: None,
338 }
339 }
340
341 fn collect_keys(&self) -> BTreeSet<String> {
342 let mut keys = BTreeSet::new();
343
344 for layer in self.layers() {
345 for entry in &layer.layer.entries {
346 keys.insert(entry.key.clone());
347 }
348 }
349
350 keys
351 }
352
353 fn drain_alias_values(
354 values: &mut BTreeMap<String, ResolvedValue>,
355 ) -> BTreeMap<String, ResolvedValue> {
356 let alias_keys = values
357 .keys()
358 .filter(|key| is_alias_key(key))
359 .cloned()
360 .collect::<Vec<_>>();
361 let mut aliases = BTreeMap::new();
362 for key in alias_keys {
363 if let Some(value) = values.remove(&key) {
364 aliases.insert(key, value);
365 }
366 }
367 aliases
368 }
369
370 fn select_across_layers<'a>(
371 &'a self,
372 key: &str,
373 selector: ScopeSelector<'a>,
374 ) -> Option<SelectedLayerEntry<'a>> {
375 let mut selected: Option<SelectedLayerEntry<'a>> = None;
376
377 for layer in self.layers() {
380 if let Some(entry) = selector.select(layer, key) {
381 if let Some(previous) = &selected {
382 if should_preserve_selected_secret(previous, &entry) {
383 tracing::trace!(
384 key = %key,
385 secret_origin = ?previous.entry.origin,
386 env_origin = ?entry.entry.origin,
387 "preserving secret env override over plain env value"
388 );
389 continue;
390 }
391 tracing::trace!(
392 key = %key,
393 previous_source = ?previous.source,
394 next_source = ?entry.source,
395 "config key winner changed across layers"
396 );
397 }
398 selected = Some(entry);
399 }
400 }
401
402 selected
403 }
404
405 fn layers(&self) -> [LayerRef<'_>; 7] {
406 [
409 LayerRef {
410 source: ConfigSource::BuiltinDefaults,
411 layer: &self.layers.defaults,
412 },
413 LayerRef {
414 source: ConfigSource::PresentationDefaults,
415 layer: &self.layers.presentation,
416 },
417 LayerRef {
418 source: ConfigSource::ConfigFile,
419 layer: &self.layers.file,
420 },
421 LayerRef {
422 source: ConfigSource::Secrets,
423 layer: &self.layers.secrets,
424 },
425 LayerRef {
426 source: ConfigSource::Environment,
427 layer: &self.layers.env,
428 },
429 LayerRef {
430 source: ConfigSource::Cli,
431 layer: &self.layers.cli,
432 },
433 LayerRef {
434 source: ConfigSource::Session,
435 layer: &self.layers.session,
436 },
437 ]
438 }
439}
440
441fn should_preserve_selected_secret(
442 previous: &SelectedLayerEntry<'_>,
443 next: &SelectedLayerEntry<'_>,
444) -> bool {
445 previous.source == ConfigSource::Secrets
446 && next.source == ConfigSource::Environment
447 && previous.entry.value.is_secret()
448 && previous
449 .entry
450 .origin
451 .as_deref()
452 .is_some_and(|origin| origin.starts_with("OSP_SECRET__"))
453}
454
455#[cfg(test)]
456mod tests {
457 use super::ConfigResolver;
458 use crate::config::{
459 ConfigError, ConfigLayer, ConfigSource, ConfigValue, ResolveOptions, Scope,
460 };
461
462 #[test]
463 fn resolver_layer_mutators_and_setters_are_callable_unit() {
464 let mut resolver = ConfigResolver::default();
465 resolver.defaults_mut().set("profile.default", "default");
466 resolver.file_mut().set("theme.name", "file");
467 resolver.secrets_mut().set("profile.default", "default");
468 resolver.env_mut().set("theme.name", "env");
469 resolver.cli_mut().set("theme.name", "cli");
470 resolver.session_mut().set("theme.name", "session");
471
472 let resolved = resolver
473 .resolve(ResolveOptions::default().with_terminal("cli"))
474 .expect("resolver should resolve");
475 assert_eq!(resolved.get_string("theme.name"), Some("session"));
476 assert_eq!(resolved.active_profile(), "default");
477
478 let mut replacement = ConfigLayer::default();
479 replacement.set("profile.default", "default");
480 replacement.set("theme.name", "replaced");
481 resolver.set_defaults(replacement);
482 resolver.set_file(ConfigLayer::default());
483 resolver.set_secrets(ConfigLayer::default());
484 resolver.set_env(ConfigLayer::default());
485 resolver.set_cli(ConfigLayer::default());
486 resolver.set_session(ConfigLayer::default());
487
488 let replaced = resolver
489 .resolve(ResolveOptions::default().with_terminal("cli"))
490 .expect("replacement config should resolve");
491 assert_eq!(replaced.get_string("theme.name"), Some("replaced"));
492 }
493
494 #[test]
495 fn explain_bootstrap_key_rejects_non_bootstrap_keys_unit() {
496 let resolver = ConfigResolver::default();
497 let err = resolver
498 .explain_bootstrap_key("ui.theme", ResolveOptions::default())
499 .expect_err("non-bootstrap key should fail");
500
501 assert!(matches!(
502 err,
503 ConfigError::InvalidConfigKey { key, .. } if key == "ui.theme"
504 ));
505 }
506
507 #[test]
508 fn resolver_keeps_secret_env_value_over_plain_env_value() {
509 let mut resolver = ConfigResolver::default();
510 resolver.defaults_mut().set("profile.default", "default");
511 resolver.secrets_mut().insert_with_origin(
512 "extensions.demo.token",
513 ConfigValue::String("secret-token".to_string()).into_secret(),
514 Scope::global(),
515 Some("OSP_SECRET__AUTH__TOKEN"),
516 );
517 resolver.env_mut().insert_with_origin(
518 "extensions.demo.token",
519 ConfigValue::String("plain-token".to_string()),
520 Scope::global(),
521 Some("OSP__AUTH__TOKEN"),
522 );
523
524 let resolved = resolver
525 .resolve(ResolveOptions::default())
526 .expect("resolver should resolve");
527 let entry = resolved
528 .get_value_entry("extensions.demo.token")
529 .expect("extensions.demo.token should resolve");
530
531 assert!(entry.value.is_secret());
532 assert_eq!(
533 entry.value.reveal(),
534 &ConfigValue::String("secret-token".to_string())
535 );
536 assert_eq!(entry.source, ConfigSource::Secrets);
537 }
538
539 #[test]
540 fn resolver_allows_selected_profile_without_scoped_entries() {
541 let mut resolver = ConfigResolver::default();
542 resolver.defaults_mut().set("profile.default", "ops");
543
544 let resolved = resolver
545 .resolve(ResolveOptions::default())
546 .expect("selected profile without scoped entries should resolve");
547
548 assert_eq!(resolved.active_profile(), "ops");
549 assert!(resolved.known_profiles().contains("ops"));
550 }
551}