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