Skip to main content

osp_cli/config/
bootstrap.rs

1use std::collections::BTreeSet;
2
3use crate::config::explain::selected_value;
4use crate::config::{
5    ActiveProfileSource, BootstrapConfigExplain, ConfigError, ConfigExplain, ConfigSource,
6    ConfigValue, ResolveOptions, ResolvedValue, Scope, normalize_identifier,
7    validate_bootstrap_value,
8};
9
10use crate::config::selector::{LayerRef, ScopeSelector};
11
12/// One resolution request always runs against a single active profile/terminal
13/// pair. Bootstrap computes this frame once up front so runtime resolution and
14/// explain both describe the same view of the world.
15#[derive(Debug, Clone)]
16pub(crate) struct ResolutionFrame {
17    pub(crate) active_profile: String,
18    pub(crate) active_profile_source: ActiveProfileSource,
19    pub(crate) terminal: Option<String>,
20    pub(crate) known_profiles: BTreeSet<String>,
21}
22
23pub(crate) fn prepare_resolution(
24    layers: [LayerRef<'_>; 7],
25    options: ResolveOptions,
26) -> Result<ResolutionFrame, ConfigError> {
27    validate_layers(layers)?;
28    let terminal = options.terminal.map(|value| normalize_identifier(&value));
29    let profile_override = options
30        .profile_override
31        .map(|value| normalize_identifier(&value));
32    let mut known_profiles = collect_known_profiles(layers);
33    let profile_selection =
34        resolve_active_profile(layers, profile_override.as_deref(), terminal.as_deref())?;
35    known_profiles.insert(profile_selection.profile.clone());
36
37    tracing::debug!(
38        active_profile = %profile_selection.profile,
39        active_profile_source = %profile_selection.source.as_str(),
40        terminal = ?terminal,
41        known_profiles = known_profiles.len(),
42        "prepared config resolution frame"
43    );
44
45    Ok(ResolutionFrame {
46        active_profile: profile_selection.profile,
47        active_profile_source: profile_selection.source,
48        terminal,
49        known_profiles,
50    })
51}
52
53pub(crate) fn explain_default_profile_key(
54    layers: [LayerRef<'_>; 7],
55    options: ResolveOptions,
56) -> Result<ConfigExplain, ConfigError> {
57    Ok(explain_default_profile_bootstrap(layers, options)?.into())
58}
59
60pub(crate) fn explain_default_profile_bootstrap(
61    layers: [LayerRef<'_>; 7],
62    options: ResolveOptions,
63) -> Result<BootstrapConfigExplain, ConfigError> {
64    let frame = prepare_resolution(layers, options)?;
65    // Default-profile lookup is a bootstrap pass, so it must not see any scope
66    // that already depends on the active profile being chosen.
67    let selector = ScopeSelector::global(frame.terminal.as_deref());
68    let explain_layers = layers
69        .into_iter()
70        .filter_map(|layer| selector.explain_layer(layer, "profile.default"))
71        .collect::<Vec<_>>();
72
73    let final_entry = select_default_profile_across_layers(layers, selector)
74        .map(|selected| selected_value(&selected))
75        .or_else(|| {
76            Some(ResolvedValue {
77                raw_value: ConfigValue::String("default".to_string()),
78                value: ConfigValue::String("default".to_string()),
79                source: ConfigSource::Derived,
80                scope: Scope::global(),
81                origin: None,
82            })
83        });
84
85    Ok(BootstrapConfigExplain {
86        key: "profile.default".to_string(),
87        active_profile: frame.active_profile,
88        active_profile_source: frame.active_profile_source,
89        terminal: frame.terminal,
90        known_profiles: frame.known_profiles,
91        layers: explain_layers,
92        final_entry,
93    })
94}
95
96impl From<BootstrapConfigExplain> for ConfigExplain {
97    fn from(value: BootstrapConfigExplain) -> Self {
98        Self {
99            key: value.key,
100            active_profile: value.active_profile,
101            active_profile_source: value.active_profile_source,
102            terminal: value.terminal,
103            known_profiles: value.known_profiles,
104            layers: value.layers,
105            final_entry: value.final_entry,
106            interpolation: None,
107        }
108    }
109}
110
111fn validate_layers(layers: [LayerRef<'_>; 7]) -> Result<(), ConfigError> {
112    for layer in layers {
113        layer.layer.validate_entries()?;
114    }
115
116    Ok(())
117}
118
119fn collect_known_profiles(layers: [LayerRef<'_>; 7]) -> BTreeSet<String> {
120    let mut known = BTreeSet::new();
121
122    for layer in layers {
123        for entry in &layer.layer.entries {
124            if let Some(profile) = entry.scope.profile.as_deref() {
125                known.insert(profile.to_string());
126            }
127        }
128    }
129
130    known
131}
132
133fn resolve_active_profile(
134    layers: [LayerRef<'_>; 7],
135    explicit: Option<&str>,
136    terminal: Option<&str>,
137) -> Result<ActiveProfileSelection, ConfigError> {
138    tracing::debug!(
139        explicit_profile = ?explicit,
140        terminal = ?terminal,
141        "resolving active profile"
142    );
143    let selection = if let Some(profile) = explicit {
144        let normalized = normalize_identifier(profile);
145        ActiveProfileSelection {
146            profile: normalized,
147            source: ActiveProfileSource::Override,
148        }
149    } else {
150        ActiveProfileSelection {
151            profile: resolve_default_profile(layers, terminal)?,
152            source: ActiveProfileSource::DefaultProfile,
153        }
154    };
155
156    if selection.profile.trim().is_empty() {
157        return Err(ConfigError::MissingDefaultProfile);
158    }
159
160    tracing::debug!(
161        active_profile = %selection.profile,
162        active_profile_source = %selection.source.as_str(),
163        "resolved active profile"
164    );
165
166    Ok(selection)
167}
168
169fn resolve_default_profile(
170    layers: [LayerRef<'_>; 7],
171    terminal: Option<&str>,
172) -> Result<String, ConfigError> {
173    let mut picked: Option<ConfigValue> = None;
174    // Bootstrap selection is layer-wide but scope-restricted: later layers may
175    // still override earlier ones, but profile-scoped values are invisible.
176    let selector = ScopeSelector::global(terminal);
177
178    for layer in layers {
179        if let Some(selected) = selector.select(layer, "profile.default") {
180            picked = Some(selected.entry.value.clone());
181        }
182    }
183
184    match picked {
185        None => {
186            tracing::debug!(terminal = ?terminal, "using implicit default profile");
187            Ok("default".to_string())
188        }
189        Some(value) => {
190            validate_bootstrap_value("profile.default", &value)?;
191            match value.reveal() {
192                ConfigValue::String(profile) => {
193                    let normalized = normalize_identifier(profile);
194                    tracing::debug!(
195                        terminal = ?terminal,
196                        selected_profile = %normalized,
197                        "resolved profile.default from loaded layers"
198                    );
199                    Ok(normalized)
200                }
201                other => Err(ConfigError::InvalidBootstrapValue {
202                    key: "profile.default".to_string(),
203                    reason: format!("expected string, got {other:?}"),
204                }),
205            }
206        }
207    }
208}
209
210fn select_default_profile_across_layers<'a>(
211    layers: [LayerRef<'a>; 7],
212    selector: ScopeSelector<'a>,
213) -> Option<crate::config::selector::SelectedLayerEntry<'a>> {
214    let mut selected = None;
215
216    for layer in layers {
217        if let Some(entry) = selector.select(layer, "profile.default") {
218            selected = Some(entry);
219        }
220    }
221
222    selected
223}
224
225#[derive(Debug, Clone)]
226struct ActiveProfileSelection {
227    profile: String,
228    source: ActiveProfileSource,
229}