Skip to main content

osp_cli/plugin/
state.rs

1use crate::config::{ConfigValue, ResolvedConfig};
2use anyhow::Result;
3use std::collections::BTreeMap;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub(crate) enum PluginCommandState {
7    Enabled,
8    Disabled,
9}
10
11impl PluginCommandState {
12    pub(crate) fn as_str(self) -> &'static str {
13        match self {
14            Self::Enabled => "enabled",
15            Self::Disabled => "disabled",
16        }
17    }
18
19    fn from_config_value(value: &ConfigValue) -> Option<Self> {
20        match value.reveal() {
21            ConfigValue::String(raw) if raw.eq_ignore_ascii_case("enabled") => Some(Self::Enabled),
22            ConfigValue::String(raw) if raw.eq_ignore_ascii_case("disabled") => {
23                Some(Self::Disabled)
24            }
25            _ => None,
26        }
27    }
28}
29
30#[derive(Debug, Clone, Default)]
31pub(crate) struct PluginCommandPreferences {
32    pub(crate) command_states: BTreeMap<String, PluginCommandState>,
33    pub(crate) preferred_providers: BTreeMap<String, String>,
34}
35
36impl PluginCommandPreferences {
37    pub(crate) fn from_resolved(config: &ResolvedConfig) -> Self {
38        let mut preferences = Self::default();
39        for (key, entry) in config.values() {
40            let Some((command, field)) = plugin_command_config_field(key) else {
41                continue;
42            };
43            match field {
44                PluginCommandConfigField::State => {
45                    if let Some(state) = PluginCommandState::from_config_value(&entry.value) {
46                        preferences.command_states.insert(command, state);
47                    }
48                }
49                PluginCommandConfigField::Provider => {
50                    if let ConfigValue::String(provider) = entry.value.reveal() {
51                        let provider = provider.trim();
52                        if !provider.is_empty() {
53                            preferences
54                                .preferred_providers
55                                .insert(command, provider.to_string());
56                        }
57                    }
58                }
59            }
60        }
61        preferences
62    }
63
64    pub(crate) fn state_for(&self, command: &str) -> Option<PluginCommandState> {
65        self.command_states.get(command).copied()
66    }
67
68    pub(crate) fn preferred_provider_for(&self, command: &str) -> Option<&str> {
69        self.preferred_providers.get(command).map(String::as_str)
70    }
71
72    #[cfg(test)]
73    pub(crate) fn set_state(&mut self, command: &str, state: PluginCommandState) {
74        self.command_states.insert(command.to_string(), state);
75    }
76
77    pub(crate) fn set_provider(&mut self, command: &str, plugin_id: &str) {
78        self.preferred_providers
79            .insert(command.to_string(), plugin_id.to_string());
80    }
81
82    pub(crate) fn clear_provider(&mut self, command: &str) -> bool {
83        self.preferred_providers.remove(command).is_some()
84    }
85}
86
87enum PluginCommandConfigField {
88    State,
89    Provider,
90}
91
92fn plugin_command_config_field(key: &str) -> Option<(String, PluginCommandConfigField)> {
93    let normalized = key.trim().to_ascii_lowercase();
94    let remainder = normalized.strip_prefix("plugins.")?;
95    let (command, field) = remainder.rsplit_once('.')?;
96    if command.trim().is_empty() {
97        return None;
98    }
99    let field = match field {
100        "state" => PluginCommandConfigField::State,
101        "provider" => PluginCommandConfigField::Provider,
102        _ => return None,
103    };
104    Some((command.to_string(), field))
105}
106
107pub(super) fn write_text_atomic(path: &std::path::Path, payload: &str) -> Result<()> {
108    crate::config::write_text_atomic(path, payload.as_bytes(), false).map_err(Into::into)
109}
110
111pub(super) fn merge_issue(target: &mut Option<String>, message: String) {
112    if message.trim().is_empty() {
113        return;
114    }
115
116    match target {
117        Some(existing) => {
118            existing.push_str("; ");
119            existing.push_str(&message);
120        }
121        None => *target = Some(message),
122    }
123}