minco-core 0.1.1

Provider-neutral application graph, static plugin composition, capabilities, and typed services for Minco
Documentation
use crate::{
    ApplicationGraph, FrozenServices, GraphBuilder, GraphError, PluginDescriptor, PluginId,
    ServiceCollection, ServiceError,
};
use serde::{Deserialize, Serialize};
use std::{
    collections::{BTreeMap, BTreeSet},
    sync::Arc,
};
use thiserror::Error;

pub trait Plugin: Send + Sync + 'static {
    fn descriptor(&self) -> PluginDescriptor;
    fn install(&self, context: &mut PluginContext<'_>) -> Result<(), PluginError>;
}

#[derive(Debug)]
pub struct PluginContext<'a> {
    services: &'a mut ServiceCollection,
}

impl PluginContext<'_> {
    pub const fn services(&mut self) -> &mut ServiceCollection {
        self.services
    }
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PluginSelection {
    #[serde(default)]
    pub enabled: BTreeSet<PluginId>,
    #[serde(default)]
    pub disabled: BTreeSet<PluginId>,
}

impl PluginSelection {
    pub fn is_enabled(&self, descriptor: &PluginDescriptor) -> bool {
        if self.disabled.contains(&descriptor.id) {
            return false;
        }
        self.enabled.contains(&descriptor.id) || descriptor.default_enabled
    }
}

#[derive(Default)]
pub struct PluginManager {
    plugins: BTreeMap<PluginId, Arc<dyn Plugin>>,
}

impl std::fmt::Debug for PluginManager {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter
            .debug_struct("PluginManager")
            .field("plugin_ids", &self.plugins.keys())
            .finish()
    }
}

impl PluginManager {
    pub fn register<P>(&mut self, plugin: P) -> Result<(), PluginError>
    where
        P: Plugin,
    {
        self.register_arc(Arc::new(plugin))
    }

    pub fn register_arc(&mut self, plugin: Arc<dyn Plugin>) -> Result<(), PluginError> {
        let id = plugin.descriptor().id;
        if self.plugins.contains_key(&id) {
            return Err(PluginError::DuplicatePlugin(id));
        }
        self.plugins.insert(id, plugin);
        Ok(())
    }

    pub fn compose(self, selection: &PluginSelection) -> Result<ComposedApplication, PluginError> {
        let enabled = self.resolve_enabled(selection)?;
        let ordered = topological_order(&enabled)?;
        let mut services = ServiceCollection::default();
        let mut graph = GraphBuilder::default();
        for id in ordered {
            let plugin = enabled
                .get(&id)
                .ok_or_else(|| PluginError::UnknownPlugin(id.clone()))?;
            let descriptor = plugin.descriptor();
            plugin.install(&mut PluginContext {
                services: &mut services,
            })?;
            graph.add_plugin(descriptor);
        }
        Ok(ComposedApplication {
            graph: graph.build()?,
            services: services.freeze(),
        })
    }

    fn resolve_enabled(
        &self,
        selection: &PluginSelection,
    ) -> Result<BTreeMap<PluginId, Arc<dyn Plugin>>, PluginError> {
        for selected in selection.enabled.iter().chain(&selection.disabled) {
            if !self.plugins.contains_key(selected) {
                return Err(PluginError::UnknownPlugin(selected.clone()));
            }
        }
        let mut enabled = BTreeMap::new();
        for (id, plugin) in &self.plugins {
            let descriptor = plugin.descriptor();
            if selection.is_enabled(&descriptor) {
                enabled.insert(id.clone(), Arc::clone(plugin));
            }
        }
        let mut changed = true;
        while changed {
            changed = false;
            let descriptors: Vec<_> = enabled.values().map(|plugin| plugin.descriptor()).collect();
            for descriptor in descriptors {
                for dependency in &descriptor.plugin_dependencies {
                    if selection.disabled.contains(dependency) {
                        return Err(PluginError::DisabledRequiredPlugin {
                            plugin: descriptor.id.clone(),
                            dependency: dependency.clone(),
                        });
                    }
                    if let std::collections::btree_map::Entry::Vacant(entry) =
                        enabled.entry(dependency.clone())
                    {
                        let plugin = self.plugins.get(dependency).ok_or_else(|| {
                            PluginError::MissingPluginDependency {
                                plugin: descriptor.id.clone(),
                                dependency: dependency.clone(),
                            }
                        })?;
                        entry.insert(Arc::clone(plugin));
                        changed = true;
                    }
                }
            }
        }
        Ok(enabled)
    }
}

fn topological_order(
    plugins: &BTreeMap<PluginId, Arc<dyn Plugin>>,
) -> Result<Vec<PluginId>, PluginError> {
    let mut visiting = BTreeSet::new();
    let mut visited = BTreeSet::new();
    let mut ordered = Vec::new();
    for id in plugins.keys() {
        visit(id, plugins, &mut visiting, &mut visited, &mut ordered)?;
    }
    Ok(ordered)
}

fn visit(
    id: &PluginId,
    plugins: &BTreeMap<PluginId, Arc<dyn Plugin>>,
    visiting: &mut BTreeSet<PluginId>,
    visited: &mut BTreeSet<PluginId>,
    ordered: &mut Vec<PluginId>,
) -> Result<(), PluginError> {
    if visited.contains(id) {
        return Ok(());
    }
    if !visiting.insert(id.clone()) {
        return Err(PluginError::DependencyCycle(id.clone()));
    }
    let plugin = plugins
        .get(id)
        .ok_or_else(|| PluginError::UnknownPlugin(id.clone()))?;
    for dependency in plugin.descriptor().plugin_dependencies {
        visit(&dependency, plugins, visiting, visited, ordered)?;
    }
    visiting.remove(id);
    visited.insert(id.clone());
    ordered.push(id.clone());
    Ok(())
}

#[derive(Debug)]
pub struct ComposedApplication {
    pub graph: ApplicationGraph,
    pub services: FrozenServices,
}

#[derive(Debug, Error)]
pub enum PluginError {
    #[error("duplicate plugin registration: {0}")]
    DuplicatePlugin(PluginId),
    #[error("unknown plugin: {0}")]
    UnknownPlugin(PluginId),
    #[error("plugin {plugin} depends on unregistered plugin {dependency}")]
    MissingPluginDependency {
        plugin: PluginId,
        dependency: PluginId,
    },
    #[error("plugin {plugin} requires disabled plugin {dependency}")]
    DisabledRequiredPlugin {
        plugin: PluginId,
        dependency: PluginId,
    },
    #[error("plugin dependency cycle includes {0}")]
    DependencyCycle(PluginId),
    #[error(transparent)]
    Service(#[from] ServiceError),
    #[error(transparent)]
    Graph(#[from] GraphError),
    #[error("plugin installation failed: {0}")]
    Installation(String),
}

#[cfg(test)]
mod tests {
    use super::*;
    use semver::Version;

    #[derive(Debug)]
    struct TestPlugin {
        descriptor: PluginDescriptor,
        value: Option<u64>,
    }
    impl Plugin for TestPlugin {
        fn descriptor(&self) -> PluginDescriptor {
            self.descriptor.clone()
        }
        fn install(&self, context: &mut PluginContext<'_>) -> Result<(), PluginError> {
            if let Some(value) = self.value {
                context.services().insert(Arc::new(value))?;
            }
            Ok(())
        }
    }
    fn plugin(id: &str, default_enabled: bool, value: Option<u64>) -> TestPlugin {
        let mut descriptor =
            PluginDescriptor::new(PluginId::new(id).unwrap(), Version::new(1, 0, 0), id);
        descriptor.default_enabled = default_enabled;
        TestPlugin { descriptor, value }
    }

    #[test]
    fn default_plugins_can_be_disabled() {
        let mut manager = PluginManager::default();
        manager.register(plugin("default", true, Some(42))).unwrap();
        let mut selection = PluginSelection::default();
        selection.disabled.insert(PluginId::new("default").unwrap());
        let composed = manager.compose(&selection).unwrap();
        assert!(composed.graph.plugins.is_empty());
    }

    #[test]
    fn duplicate_registration_does_not_replace_the_original_plugin() {
        let mut manager = PluginManager::default();
        manager.register(plugin("service", true, Some(1))).unwrap();
        assert!(matches!(
            manager.register(plugin("service", true, Some(2))),
            Err(PluginError::DuplicatePlugin(_))
        ));
        let composed = manager.compose(&PluginSelection::default()).unwrap();
        assert_eq!(*composed.services.get::<u64>().unwrap(), 1);
    }

    #[test]
    fn unknown_runtime_selection_fails_closed() {
        let manager = PluginManager::default();
        let mut selection = PluginSelection::default();
        selection.enabled.insert(PluginId::new("missing").unwrap());
        assert!(matches!(
            manager.compose(&selection),
            Err(PluginError::UnknownPlugin(_))
        ));
    }

    #[test]
    fn explicit_plugin_install_exposes_typed_service() {
        let mut manager = PluginManager::default();
        manager
            .register(plugin("service", false, Some(42)))
            .unwrap();
        let mut selection = PluginSelection::default();
        selection.enabled.insert(PluginId::new("service").unwrap());
        let composed = manager.compose(&selection).unwrap();
        assert_eq!(*composed.services.get::<u64>().unwrap(), 42);
    }
}