osp-cli 1.5.1

CLI and REPL for querying and managing OSP infrastructure data
Documentation
use crate::config::{ConfigValue, ResolvedConfig};
use anyhow::Result;
use std::collections::BTreeMap;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PluginCommandState {
    Enabled,
    Disabled,
}

impl PluginCommandState {
    pub(crate) fn as_str(self) -> &'static str {
        match self {
            Self::Enabled => "enabled",
            Self::Disabled => "disabled",
        }
    }

    fn from_config_value(value: &ConfigValue) -> Option<Self> {
        match value.reveal() {
            ConfigValue::String(raw) if raw.eq_ignore_ascii_case("enabled") => Some(Self::Enabled),
            ConfigValue::String(raw) if raw.eq_ignore_ascii_case("disabled") => {
                Some(Self::Disabled)
            }
            _ => None,
        }
    }
}

#[derive(Debug, Clone, Default)]
pub(crate) struct PluginCommandPreferences {
    pub(crate) command_states: BTreeMap<String, PluginCommandState>,
    pub(crate) preferred_providers: BTreeMap<String, String>,
}

impl PluginCommandPreferences {
    pub(crate) fn from_resolved(config: &ResolvedConfig) -> Self {
        let mut preferences = Self::default();
        for (key, entry) in config.values() {
            let Some((command, field)) = plugin_command_config_field(key) else {
                continue;
            };
            match field {
                PluginCommandConfigField::State => {
                    if let Some(state) = PluginCommandState::from_config_value(&entry.value) {
                        preferences.command_states.insert(command, state);
                    }
                }
                PluginCommandConfigField::Provider => {
                    if let ConfigValue::String(provider) = entry.value.reveal() {
                        let provider = provider.trim();
                        if !provider.is_empty() {
                            preferences
                                .preferred_providers
                                .insert(command, provider.to_string());
                        }
                    }
                }
            }
        }
        preferences
    }

    pub(crate) fn state_for(&self, command: &str) -> Option<PluginCommandState> {
        self.command_states.get(command).copied()
    }

    pub(crate) fn preferred_provider_for(&self, command: &str) -> Option<&str> {
        self.preferred_providers.get(command).map(String::as_str)
    }

    #[cfg(test)]
    pub(crate) fn set_state(&mut self, command: &str, state: PluginCommandState) {
        self.command_states.insert(command.to_string(), state);
    }

    pub(crate) fn set_provider(&mut self, command: &str, plugin_id: &str) {
        self.preferred_providers
            .insert(command.to_string(), plugin_id.to_string());
    }

    pub(crate) fn clear_provider(&mut self, command: &str) -> bool {
        self.preferred_providers.remove(command).is_some()
    }
}

enum PluginCommandConfigField {
    State,
    Provider,
}

fn plugin_command_config_field(key: &str) -> Option<(String, PluginCommandConfigField)> {
    let normalized = key.trim().to_ascii_lowercase();
    let remainder = normalized.strip_prefix("plugins.")?;
    let (command, field) = remainder.rsplit_once('.')?;
    if command.trim().is_empty() {
        return None;
    }
    let field = match field {
        "state" => PluginCommandConfigField::State,
        "provider" => PluginCommandConfigField::Provider,
        _ => return None,
    };
    Some((command.to_string(), field))
}

pub(super) fn write_text_atomic(path: &std::path::Path, payload: &str) -> Result<()> {
    crate::config::write_text_atomic(path, payload.as_bytes(), false).map_err(Into::into)
}

pub(super) fn merge_issue(target: &mut Option<String>, message: String) {
    if message.trim().is_empty() {
        return;
    }

    match target {
        Some(existing) => {
            existing.push_str("; ");
            existing.push_str(&message);
        }
        None => *target = Some(message),
    }
}