osp-cli 1.5.1

CLI and REPL for querying and managing OSP infrastructure data
Documentation
use super::manager::DiscoveredPlugin;
use super::state::{PluginCommandPreferences, PluginCommandState};
use std::collections::HashMap;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ProviderSelectionMode {
    Override,
    Preference,
    Unique,
}

#[derive(Debug, Clone, Copy)]
pub(crate) struct ProviderSelection<'a> {
    pub(crate) plugin: &'a DiscoveredPlugin,
    pub(crate) mode: ProviderSelectionMode,
}

pub(crate) enum ProviderResolution<'a> {
    Selected(ProviderSelection<'a>),
    Ambiguous(Vec<&'a DiscoveredPlugin>),
}

#[derive(Debug)]
pub(crate) enum ProviderResolutionError<'a> {
    CommandNotFound,
    RequestedProviderUnavailable {
        requested_provider: String,
        providers: Vec<&'a DiscoveredPlugin>,
    },
}

pub(crate) fn healthy_plugins(
    discovered: &[DiscoveredPlugin],
) -> impl Iterator<Item = &DiscoveredPlugin> + '_ {
    discovered.iter().filter(|plugin| plugin.issue.is_none())
}

pub(crate) fn plugin_enabled(
    plugin: &DiscoveredPlugin,
    preferences: &PluginCommandPreferences,
) -> bool {
    let commands = plugin.canonical_command_names();
    if commands.is_empty() {
        return plugin.default_enabled;
    }
    commands
        .into_iter()
        .any(|command| provider_available(plugin, command, preferences))
}

pub(crate) fn plugin_label(plugin: &DiscoveredPlugin) -> String {
    format!("{} ({})", plugin.plugin_id, plugin.source)
}

pub(crate) fn provider_labels(plugins: &[&DiscoveredPlugin]) -> Vec<String> {
    plugins.iter().copied().map(plugin_label).collect()
}

pub(crate) fn providers_for_command<'a>(
    command: &str,
    plugins: &[&'a DiscoveredPlugin],
) -> Vec<&'a DiscoveredPlugin> {
    plugins
        .iter()
        .copied()
        .filter(|plugin| plugin_provides_command(plugin, command))
        .collect()
}

pub(crate) fn provider_labels_by_command(
    plugins: &[&DiscoveredPlugin],
    preferences: &PluginCommandPreferences,
) -> HashMap<String, Vec<String>> {
    let mut index = HashMap::new();
    for plugin in plugins {
        let label = plugin_label(plugin);
        for command in plugin.canonical_command_names() {
            if !provider_available(plugin, command, preferences) {
                continue;
            }
            index
                .entry(command.to_string())
                .or_insert_with(Vec::new)
                .push(label.clone());
        }
    }
    index
}

pub(crate) fn provider_available(
    plugin: &DiscoveredPlugin,
    command: &str,
    preferences: &PluginCommandPreferences,
) -> bool {
    if plugin.canonical_command(command).is_none() {
        return false;
    }
    match preferences.state_for(command) {
        Some(PluginCommandState::Enabled) => true,
        Some(PluginCommandState::Disabled) => false,
        None => plugin.default_enabled,
    }
}

pub(crate) fn resolve_provider_for_command<'a>(
    command: &str,
    plugins: &[&'a DiscoveredPlugin],
    preferences: &PluginCommandPreferences,
    provider_override: Option<&str>,
) -> std::result::Result<ProviderResolution<'a>, ProviderResolutionError<'a>> {
    let providers = available_providers_for_command(command, plugins, preferences);
    if providers.is_empty() {
        return Err(ProviderResolutionError::CommandNotFound);
    }

    if let Some(requested_provider) = provider_override
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        if let Some(plugin) = providers
            .iter()
            .copied()
            .find(|plugin| plugin.plugin_id == requested_provider)
        {
            return Ok(ProviderResolution::Selected(ProviderSelection {
                plugin,
                mode: ProviderSelectionMode::Override,
            }));
        }
        return Err(ProviderResolutionError::RequestedProviderUnavailable {
            requested_provider: requested_provider.to_string(),
            providers,
        });
    }

    if let Some(preferred) = preferences.preferred_provider_for(command) {
        if let Some(plugin) = providers
            .iter()
            .copied()
            .find(|plugin| plugin.plugin_id == preferred)
        {
            return Ok(ProviderResolution::Selected(ProviderSelection {
                plugin,
                mode: ProviderSelectionMode::Preference,
            }));
        }

        tracing::trace!(
            command = %command,
            preferred_provider = %preferred,
            available_providers = providers.len(),
            "preferred provider not available; reevaluating command provider"
        );
    }

    if providers.len() == 1 {
        return Ok(ProviderResolution::Selected(ProviderSelection {
            plugin: providers[0],
            mode: ProviderSelectionMode::Unique,
        }));
    }

    Ok(ProviderResolution::Ambiguous(providers))
}

fn plugin_provides_command(plugin: &DiscoveredPlugin, command: &str) -> bool {
    plugin.canonical_command(command).is_some()
}

pub(crate) fn available_providers_for_command<'a>(
    command: &str,
    plugins: &[&'a DiscoveredPlugin],
    preferences: &PluginCommandPreferences,
) -> Vec<&'a DiscoveredPlugin> {
    providers_for_command(command, plugins)
        .into_iter()
        .filter(|plugin| provider_available(plugin, command, preferences))
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::completion::CommandSpec;
    use crate::plugin::{DiscoveredPlugin, PluginSource};
    use std::path::PathBuf;

    fn plugin(plugin_id: &str, command: &str) -> DiscoveredPlugin {
        DiscoveredPlugin {
            plugin_id: plugin_id.to_string(),
            plugin_version: Some("0.1.0".to_string()),
            executable: PathBuf::from(format!("/tmp/osp-{plugin_id}")),
            source: PluginSource::Explicit,
            commands: vec![command.to_string()],
            describe_commands: Vec::new(),
            command_specs: vec![CommandSpec::new(command)],
            issue: None,
            default_enabled: true,
        }
    }

    #[test]
    fn provider_resolution_prefers_override_then_preference_then_unique_unit() {
        let alpha = plugin("alpha", "shared");
        let beta = plugin("beta", "shared");
        let unique = plugin("solo", "solo");
        let shared_plugins = vec![&alpha, &beta];
        let unique_plugins = vec![&unique];
        let mut preferences = PluginCommandPreferences::default();

        let ProviderResolution::Ambiguous(providers) =
            resolve_provider_for_command("shared", &shared_plugins, &preferences, None)
                .expect("shared command should resolve")
        else {
            panic!("shared command should be ambiguous without overrides");
        };
        assert_eq!(providers.len(), 2);

        preferences.set_provider("shared", "beta");
        let ProviderResolution::Selected(selection) =
            resolve_provider_for_command("shared", &shared_plugins, &preferences, None)
                .expect("preferred provider should resolve")
        else {
            panic!("preferred provider should select one plugin");
        };
        assert_eq!(selection.plugin.plugin_id, "beta");
        assert_eq!(selection.mode, ProviderSelectionMode::Preference);

        let ProviderResolution::Selected(selection) =
            resolve_provider_for_command("shared", &shared_plugins, &preferences, Some("alpha"))
                .expect("override should resolve")
        else {
            panic!("override should select one plugin");
        };
        assert_eq!(selection.plugin.plugin_id, "alpha");
        assert_eq!(selection.mode, ProviderSelectionMode::Override);

        let ProviderResolution::Selected(selection) =
            resolve_provider_for_command("solo", &unique_plugins, &preferences, None)
                .expect("unique provider should resolve")
        else {
            panic!("unique provider should select one plugin");
        };
        assert_eq!(selection.plugin.plugin_id, "solo");
        assert_eq!(selection.mode, ProviderSelectionMode::Unique);
    }

    #[test]
    fn provider_availability_honors_command_state_over_default_enabled_unit() {
        let plugin = plugin("alpha", "shared");
        let mut preferences = PluginCommandPreferences::default();

        assert!(provider_available(&plugin, "shared", &preferences));

        preferences.set_state("shared", PluginCommandState::Disabled);
        assert!(!provider_available(&plugin, "shared", &preferences));

        preferences.set_state("shared", PluginCommandState::Enabled);
        assert!(provider_available(&plugin, "shared", &preferences));
    }

    #[test]
    fn canonical_command_identity_drives_provider_lookup_unit() {
        let plugin = DiscoveredPlugin {
            plugin_id: "alpha".to_string(),
            plugin_version: Some("0.1.0".to_string()),
            executable: PathBuf::from("/tmp/osp-alpha"),
            source: PluginSource::Explicit,
            commands: Vec::new(),
            describe_commands: Vec::new(),
            command_specs: vec![CommandSpec::new("shared")],
            issue: None,
            default_enabled: true,
        };
        let preferences = PluginCommandPreferences::default();

        assert!(provider_available(&plugin, "shared", &preferences));
        assert_eq!(
            providers_for_command("shared", &[&plugin])
                .into_iter()
                .map(|provider| provider.plugin_id.as_str())
                .collect::<Vec<_>>(),
            vec!["alpha"]
        );
    }
}