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