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}