Skip to main content

minco_core/
plugin.rs

1use crate::{
2    ApplicationGraph, FrozenServices, GraphBuilder, GraphError, PluginDescriptor, PluginId,
3    ServiceCollection, ServiceError,
4};
5use serde::{Deserialize, Serialize};
6use std::{
7    collections::{BTreeMap, BTreeSet},
8    sync::Arc,
9};
10use thiserror::Error;
11
12pub trait Plugin: Send + Sync + 'static {
13    fn descriptor(&self) -> PluginDescriptor;
14    fn install(&self, context: &mut PluginContext<'_>) -> Result<(), PluginError>;
15}
16
17#[derive(Debug)]
18pub struct PluginContext<'a> {
19    services: &'a mut ServiceCollection,
20}
21
22impl PluginContext<'_> {
23    pub const fn services(&mut self) -> &mut ServiceCollection {
24        self.services
25    }
26}
27
28#[derive(Debug, Clone, Default, Serialize, Deserialize)]
29pub struct PluginSelection {
30    #[serde(default)]
31    pub enabled: BTreeSet<PluginId>,
32    #[serde(default)]
33    pub disabled: BTreeSet<PluginId>,
34}
35
36impl PluginSelection {
37    pub fn is_enabled(&self, descriptor: &PluginDescriptor) -> bool {
38        if self.disabled.contains(&descriptor.id) {
39            return false;
40        }
41        self.enabled.contains(&descriptor.id) || descriptor.default_enabled
42    }
43}
44
45#[derive(Default)]
46pub struct PluginManager {
47    plugins: BTreeMap<PluginId, Arc<dyn Plugin>>,
48}
49
50impl std::fmt::Debug for PluginManager {
51    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        formatter
53            .debug_struct("PluginManager")
54            .field("plugin_ids", &self.plugins.keys())
55            .finish()
56    }
57}
58
59impl PluginManager {
60    pub fn register<P>(&mut self, plugin: P) -> Result<(), PluginError>
61    where
62        P: Plugin,
63    {
64        self.register_arc(Arc::new(plugin))
65    }
66
67    pub fn register_arc(&mut self, plugin: Arc<dyn Plugin>) -> Result<(), PluginError> {
68        let id = plugin.descriptor().id;
69        if self.plugins.contains_key(&id) {
70            return Err(PluginError::DuplicatePlugin(id));
71        }
72        self.plugins.insert(id, plugin);
73        Ok(())
74    }
75
76    pub fn compose(self, selection: &PluginSelection) -> Result<ComposedApplication, PluginError> {
77        let enabled = self.resolve_enabled(selection)?;
78        let ordered = topological_order(&enabled)?;
79        let mut services = ServiceCollection::default();
80        let mut graph = GraphBuilder::default();
81        for id in ordered {
82            let plugin = enabled
83                .get(&id)
84                .ok_or_else(|| PluginError::UnknownPlugin(id.clone()))?;
85            let descriptor = plugin.descriptor();
86            plugin.install(&mut PluginContext {
87                services: &mut services,
88            })?;
89            graph.add_plugin(descriptor);
90        }
91        Ok(ComposedApplication {
92            graph: graph.build()?,
93            services: services.freeze(),
94        })
95    }
96
97    fn resolve_enabled(
98        &self,
99        selection: &PluginSelection,
100    ) -> Result<BTreeMap<PluginId, Arc<dyn Plugin>>, PluginError> {
101        for selected in selection.enabled.iter().chain(&selection.disabled) {
102            if !self.plugins.contains_key(selected) {
103                return Err(PluginError::UnknownPlugin(selected.clone()));
104            }
105        }
106        let mut enabled = BTreeMap::new();
107        for (id, plugin) in &self.plugins {
108            let descriptor = plugin.descriptor();
109            if selection.is_enabled(&descriptor) {
110                enabled.insert(id.clone(), Arc::clone(plugin));
111            }
112        }
113        let mut changed = true;
114        while changed {
115            changed = false;
116            let descriptors: Vec<_> = enabled.values().map(|plugin| plugin.descriptor()).collect();
117            for descriptor in descriptors {
118                for dependency in &descriptor.plugin_dependencies {
119                    if selection.disabled.contains(dependency) {
120                        return Err(PluginError::DisabledRequiredPlugin {
121                            plugin: descriptor.id.clone(),
122                            dependency: dependency.clone(),
123                        });
124                    }
125                    if let std::collections::btree_map::Entry::Vacant(entry) =
126                        enabled.entry(dependency.clone())
127                    {
128                        let plugin = self.plugins.get(dependency).ok_or_else(|| {
129                            PluginError::MissingPluginDependency {
130                                plugin: descriptor.id.clone(),
131                                dependency: dependency.clone(),
132                            }
133                        })?;
134                        entry.insert(Arc::clone(plugin));
135                        changed = true;
136                    }
137                }
138            }
139        }
140        Ok(enabled)
141    }
142}
143
144fn topological_order(
145    plugins: &BTreeMap<PluginId, Arc<dyn Plugin>>,
146) -> Result<Vec<PluginId>, PluginError> {
147    let mut visiting = BTreeSet::new();
148    let mut visited = BTreeSet::new();
149    let mut ordered = Vec::new();
150    for id in plugins.keys() {
151        visit(id, plugins, &mut visiting, &mut visited, &mut ordered)?;
152    }
153    Ok(ordered)
154}
155
156fn visit(
157    id: &PluginId,
158    plugins: &BTreeMap<PluginId, Arc<dyn Plugin>>,
159    visiting: &mut BTreeSet<PluginId>,
160    visited: &mut BTreeSet<PluginId>,
161    ordered: &mut Vec<PluginId>,
162) -> Result<(), PluginError> {
163    if visited.contains(id) {
164        return Ok(());
165    }
166    if !visiting.insert(id.clone()) {
167        return Err(PluginError::DependencyCycle(id.clone()));
168    }
169    let plugin = plugins
170        .get(id)
171        .ok_or_else(|| PluginError::UnknownPlugin(id.clone()))?;
172    for dependency in plugin.descriptor().plugin_dependencies {
173        visit(&dependency, plugins, visiting, visited, ordered)?;
174    }
175    visiting.remove(id);
176    visited.insert(id.clone());
177    ordered.push(id.clone());
178    Ok(())
179}
180
181#[derive(Debug)]
182pub struct ComposedApplication {
183    pub graph: ApplicationGraph,
184    pub services: FrozenServices,
185}
186
187#[derive(Debug, Error)]
188pub enum PluginError {
189    #[error("duplicate plugin registration: {0}")]
190    DuplicatePlugin(PluginId),
191    #[error("unknown plugin: {0}")]
192    UnknownPlugin(PluginId),
193    #[error("plugin {plugin} depends on unregistered plugin {dependency}")]
194    MissingPluginDependency {
195        plugin: PluginId,
196        dependency: PluginId,
197    },
198    #[error("plugin {plugin} requires disabled plugin {dependency}")]
199    DisabledRequiredPlugin {
200        plugin: PluginId,
201        dependency: PluginId,
202    },
203    #[error("plugin dependency cycle includes {0}")]
204    DependencyCycle(PluginId),
205    #[error(transparent)]
206    Service(#[from] ServiceError),
207    #[error(transparent)]
208    Graph(#[from] GraphError),
209    #[error("plugin installation failed: {0}")]
210    Installation(String),
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use semver::Version;
217
218    #[derive(Debug)]
219    struct TestPlugin {
220        descriptor: PluginDescriptor,
221        value: Option<u64>,
222    }
223    impl Plugin for TestPlugin {
224        fn descriptor(&self) -> PluginDescriptor {
225            self.descriptor.clone()
226        }
227        fn install(&self, context: &mut PluginContext<'_>) -> Result<(), PluginError> {
228            if let Some(value) = self.value {
229                context.services().insert(Arc::new(value))?;
230            }
231            Ok(())
232        }
233    }
234    fn plugin(id: &str, default_enabled: bool, value: Option<u64>) -> TestPlugin {
235        let mut descriptor =
236            PluginDescriptor::new(PluginId::new(id).unwrap(), Version::new(1, 0, 0), id);
237        descriptor.default_enabled = default_enabled;
238        TestPlugin { descriptor, value }
239    }
240
241    #[test]
242    fn default_plugins_can_be_disabled() {
243        let mut manager = PluginManager::default();
244        manager.register(plugin("default", true, Some(42))).unwrap();
245        let mut selection = PluginSelection::default();
246        selection.disabled.insert(PluginId::new("default").unwrap());
247        let composed = manager.compose(&selection).unwrap();
248        assert!(composed.graph.plugins.is_empty());
249    }
250
251    #[test]
252    fn duplicate_registration_does_not_replace_the_original_plugin() {
253        let mut manager = PluginManager::default();
254        manager.register(plugin("service", true, Some(1))).unwrap();
255        assert!(matches!(
256            manager.register(plugin("service", true, Some(2))),
257            Err(PluginError::DuplicatePlugin(_))
258        ));
259        let composed = manager.compose(&PluginSelection::default()).unwrap();
260        assert_eq!(*composed.services.get::<u64>().unwrap(), 1);
261    }
262
263    #[test]
264    fn unknown_runtime_selection_fails_closed() {
265        let manager = PluginManager::default();
266        let mut selection = PluginSelection::default();
267        selection.enabled.insert(PluginId::new("missing").unwrap());
268        assert!(matches!(
269            manager.compose(&selection),
270            Err(PluginError::UnknownPlugin(_))
271        ));
272    }
273
274    #[test]
275    fn explicit_plugin_install_exposes_typed_service() {
276        let mut manager = PluginManager::default();
277        manager
278            .register(plugin("service", false, Some(42)))
279            .unwrap();
280        let mut selection = PluginSelection::default();
281        selection.enabled.insert(PluginId::new("service").unwrap());
282        let composed = manager.compose(&selection).unwrap();
283        assert_eq!(*composed.services.get::<u64>().unwrap(), 42);
284    }
285}