1use crate::{
2 ApplicationGraph, CORE_API_VERSION, ConfigurationField, ConfigurationValueKind,
3 ContributionCollection, FrozenContributions, FrozenServices, GraphBuilder, GraphError,
4 PluginDescriptor, PluginId, ServiceCollection, ServiceError,
5};
6use semver::Version;
7use serde::{Deserialize, Serialize, de::DeserializeOwned};
8use std::{
9 collections::{BTreeMap, BTreeSet},
10 sync::Arc,
11};
12use thiserror::Error;
13
14pub trait Plugin: Send + Sync + 'static {
20 fn descriptor(&self) -> PluginDescriptor;
21
22 fn configure_descriptor(
30 &self,
31 _descriptor: &mut PluginDescriptor,
32 _configuration: Option<&serde_json::Value>,
33 ) -> Result<(), PluginError> {
34 Ok(())
35 }
36
37 fn install(&self, context: &mut PluginContext<'_>) -> Result<(), PluginError>;
38
39 fn finalize(&self, _context: &mut PluginFinalizeContext<'_>) -> Result<(), PluginError> {
46 Ok(())
47 }
48}
49
50#[derive(Debug)]
51pub struct PluginContext<'a> {
52 plugin_id: &'a PluginId,
53 configuration: Option<&'a serde_json::Value>,
54 services: &'a mut ServiceCollection,
55 contributions: &'a mut ContributionCollection,
56}
57
58#[derive(Debug)]
63pub struct PluginFinalizeContext<'a> {
64 plugin_id: &'a PluginId,
65 configuration: Option<&'a serde_json::Value>,
66 services: &'a mut ServiceCollection,
67 contributions: &'a ContributionCollection,
68}
69
70impl PluginFinalizeContext<'_> {
71 pub const fn plugin_id(&self) -> &PluginId {
72 self.plugin_id
73 }
74
75 pub const fn services(&mut self) -> &mut ServiceCollection {
76 self.services
77 }
78
79 pub const fn contributions(&self) -> &ContributionCollection {
80 self.contributions
81 }
82
83 pub const fn raw_configuration(&self) -> Option<&serde_json::Value> {
84 self.configuration
85 }
86
87 pub fn configuration<T>(&self) -> Result<T, PluginError>
88 where
89 T: DeserializeOwned + Default,
90 {
91 deserialize_configuration(self.plugin_id, self.configuration)
92 }
93}
94
95impl PluginContext<'_> {
96 pub const fn plugin_id(&self) -> &PluginId {
97 self.plugin_id
98 }
99
100 pub const fn services(&mut self) -> &mut ServiceCollection {
101 self.services
102 }
103
104 pub const fn contributions(&mut self) -> &mut ContributionCollection {
105 self.contributions
106 }
107
108 pub const fn raw_configuration(&self) -> Option<&serde_json::Value> {
109 self.configuration
110 }
111
112 pub fn configuration<T>(&self) -> Result<T, PluginError>
115 where
116 T: DeserializeOwned + Default,
117 {
118 deserialize_configuration(self.plugin_id, self.configuration)
119 }
120}
121
122fn deserialize_configuration<T>(
123 plugin_id: &PluginId,
124 configuration: Option<&serde_json::Value>,
125) -> Result<T, PluginError>
126where
127 T: DeserializeOwned + Default,
128{
129 configuration.map_or_else(
130 || Ok(T::default()),
131 |value| {
132 serde_json::from_value(value.clone()).map_err(|source| {
133 PluginError::InvalidConfiguration {
134 plugin: plugin_id.clone(),
135 source,
136 }
137 })
138 },
139 )
140}
141
142#[derive(Debug, Clone, Default, Serialize, Deserialize)]
143pub struct PluginSelection {
144 #[serde(default)]
145 pub enabled: BTreeSet<PluginId>,
146 #[serde(default)]
147 pub disabled: BTreeSet<PluginId>,
148 #[serde(default)]
150 pub configuration: BTreeMap<PluginId, serde_json::Value>,
151}
152
153impl PluginSelection {
154 pub fn is_enabled(&self, descriptor: &PluginDescriptor) -> bool {
155 if self.disabled.contains(&descriptor.id) {
156 return false;
157 }
158 self.enabled.contains(&descriptor.id) || descriptor.default_enabled
159 }
160
161 pub fn set_configuration<T>(
162 &mut self,
163 plugin_id: PluginId,
164 configuration: &T,
165 ) -> Result<(), PluginError>
166 where
167 T: Serialize,
168 {
169 let value = serde_json::to_value(configuration).map_err(|source| {
170 PluginError::InvalidConfiguration {
171 plugin: plugin_id.clone(),
172 source,
173 }
174 })?;
175 self.configuration.insert(plugin_id, value);
176 Ok(())
177 }
178}
179
180struct RegisteredPlugin {
181 plugin: Arc<dyn Plugin>,
182 descriptor: PluginDescriptor,
183}
184
185impl std::fmt::Debug for RegisteredPlugin {
186 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187 formatter
188 .debug_struct("RegisteredPlugin")
189 .field("descriptor", &self.descriptor)
190 .finish_non_exhaustive()
191 }
192}
193
194#[derive(Clone)]
195struct EffectivePlugin {
196 plugin: Arc<dyn Plugin>,
197 descriptor: PluginDescriptor,
198 configuration: Option<serde_json::Value>,
199}
200
201impl std::fmt::Debug for EffectivePlugin {
202 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203 formatter
204 .debug_struct("EffectivePlugin")
205 .field("descriptor", &self.descriptor)
206 .field("configuration_present", &self.configuration.is_some())
207 .finish_non_exhaustive()
208 }
209}
210
211struct ResolvedGraph {
212 enabled: BTreeMap<PluginId, EffectivePlugin>,
213 ordered: Vec<PluginId>,
214 graph: ApplicationGraph,
215}
216
217#[derive(Default)]
218pub struct PluginManager {
219 plugins: BTreeMap<PluginId, RegisteredPlugin>,
220}
221
222impl std::fmt::Debug for PluginManager {
223 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224 formatter
225 .debug_struct("PluginManager")
226 .field("plugin_ids", &self.plugins.keys())
227 .finish()
228 }
229}
230
231impl PluginManager {
232 pub fn register<P>(&mut self, plugin: P) -> Result<(), PluginError>
233 where
234 P: Plugin,
235 {
236 self.register_arc(Arc::new(plugin))
237 }
238
239 pub fn register_arc(&mut self, plugin: Arc<dyn Plugin>) -> Result<(), PluginError> {
240 let descriptor = plugin.descriptor();
241 validate_configuration_descriptor(&descriptor)?;
242 let core_version =
243 Version::parse(CORE_API_VERSION).map_err(|source| PluginError::InvalidCoreVersion {
244 value: CORE_API_VERSION.to_owned(),
245 source,
246 })?;
247 if !descriptor.core_compatibility.matches(&core_version) {
248 return Err(PluginError::IncompatibleCore {
249 plugin: descriptor.id,
250 requirement: descriptor.core_compatibility.to_string(),
251 actual: core_version,
252 });
253 }
254 if self.plugins.contains_key(&descriptor.id) {
255 return Err(PluginError::DuplicatePlugin(descriptor.id));
256 }
257 self.plugins.insert(
258 descriptor.id.clone(),
259 RegisteredPlugin { plugin, descriptor },
260 );
261 Ok(())
262 }
263
264 pub fn descriptors(&self) -> Vec<PluginDescriptor> {
265 self.plugins
266 .values()
267 .map(|registration| registration.descriptor.clone())
268 .collect()
269 }
270
271 pub fn compose(&self, selection: &PluginSelection) -> Result<ComposedApplication, PluginError> {
272 self.compose_with(
273 selection,
274 ServiceCollection::default(),
275 ContributionCollection::default(),
276 )
277 }
278
279 pub fn build_graph(
284 &self,
285 selection: &PluginSelection,
286 ) -> Result<ApplicationGraph, PluginError> {
287 Ok(self.resolve_graph(selection)?.graph)
288 }
289
290 pub fn compose_with(
296 &self,
297 selection: &PluginSelection,
298 mut services: ServiceCollection,
299 mut contributions: ContributionCollection,
300 ) -> Result<ComposedApplication, PluginError> {
301 let ResolvedGraph {
304 enabled,
305 ordered,
306 graph,
307 } = self.resolve_graph(selection)?;
308
309 for id in &ordered {
310 let effective = enabled
311 .get(id)
312 .ok_or_else(|| PluginError::UnknownPlugin(id.clone()))?;
313 effective.plugin.install(&mut PluginContext {
314 plugin_id: id,
315 configuration: effective.configuration.as_ref(),
316 services: &mut services,
317 contributions: &mut contributions,
318 })?;
319 }
320
321 for id in &ordered {
322 let effective = enabled
323 .get(id)
324 .ok_or_else(|| PluginError::UnknownPlugin(id.clone()))?;
325 effective.plugin.finalize(&mut PluginFinalizeContext {
326 plugin_id: id,
327 configuration: effective.configuration.as_ref(),
328 services: &mut services,
329 contributions: &contributions,
330 })?;
331 }
332
333 Ok(ComposedApplication {
334 graph,
335 services: services.freeze(),
336 contributions: contributions.freeze(),
337 })
338 }
339
340 fn resolve_graph(&self, selection: &PluginSelection) -> Result<ResolvedGraph, PluginError> {
341 self.validate_selection(selection)?;
342 let enabled = self.resolve_enabled(selection)?;
343 let ordered = topological_order(&enabled)?;
344 let mut graph_builder = GraphBuilder::default();
345 for id in &ordered {
346 let effective = enabled
347 .get(id)
348 .ok_or_else(|| PluginError::UnknownPlugin(id.clone()))?;
349 graph_builder.add_plugin(effective.descriptor.clone());
350 }
351 let graph = graph_builder.build()?;
352 Ok(ResolvedGraph {
353 enabled,
354 ordered,
355 graph,
356 })
357 }
358
359 fn validate_selection(&self, selection: &PluginSelection) -> Result<(), PluginError> {
360 if let Some(id) = selection.enabled.intersection(&selection.disabled).next() {
361 return Err(PluginError::ContradictorySelection(id.clone()));
362 }
363 for selected in selection
364 .enabled
365 .iter()
366 .chain(&selection.disabled)
367 .chain(selection.configuration.keys())
368 {
369 if !self.plugins.contains_key(selected) {
370 return Err(PluginError::UnknownPlugin(selected.clone()));
371 }
372 }
373 Ok(())
374 }
375
376 fn resolve_enabled(
377 &self,
378 selection: &PluginSelection,
379 ) -> Result<BTreeMap<PluginId, EffectivePlugin>, PluginError> {
380 let mut enabled = BTreeMap::new();
381 for (id, registration) in &self.plugins {
382 if selection.is_enabled(®istration.descriptor) {
383 enabled.insert(id.clone(), self.effective_plugin(id, selection)?);
384 }
385 }
386
387 let mut changed = true;
388 while changed {
389 changed = false;
390 let descriptors = enabled
391 .values()
392 .map(|effective| effective.descriptor.clone())
393 .collect::<Vec<_>>();
394 for descriptor in descriptors {
395 for dependency in descriptor.plugin_dependencies {
396 if selection.disabled.contains(&dependency) {
397 return Err(PluginError::DisabledRequiredPlugin {
398 plugin: descriptor.id,
399 dependency,
400 });
401 }
402 if !enabled.contains_key(&dependency) {
403 if !self.plugins.contains_key(&dependency) {
404 return Err(PluginError::MissingPluginDependency {
405 plugin: descriptor.id,
406 dependency,
407 });
408 }
409 enabled.insert(
410 dependency.clone(),
411 self.effective_plugin(&dependency, selection)?,
412 );
413 changed = true;
414 }
415 }
416 }
417 }
418 Ok(enabled)
419 }
420
421 fn effective_plugin(
422 &self,
423 id: &PluginId,
424 selection: &PluginSelection,
425 ) -> Result<EffectivePlugin, PluginError> {
426 let registration = self
427 .plugins
428 .get(id)
429 .ok_or_else(|| PluginError::UnknownPlugin(id.clone()))?;
430 let configuration =
431 normalize_configuration(®istration.descriptor, selection.configuration.get(id))?;
432 let mut descriptor = registration.descriptor.clone();
433 registration
434 .plugin
435 .configure_descriptor(&mut descriptor, configuration.as_ref())?;
436 validate_configured_descriptor(®istration.descriptor, &descriptor)?;
437 Ok(EffectivePlugin {
438 plugin: Arc::clone(®istration.plugin),
439 descriptor,
440 configuration,
441 })
442 }
443}
444
445fn validate_configuration_descriptor(descriptor: &PluginDescriptor) -> Result<(), PluginError> {
446 let mut keys = BTreeSet::new();
447 for field in &descriptor.configuration {
448 if field.key.trim().is_empty() {
449 return Err(PluginError::InvalidConfigurationDescriptor {
450 plugin: descriptor.id.clone(),
451 message: "configuration field keys must not be empty".into(),
452 });
453 }
454 if !keys.insert(field.key.clone()) {
455 return Err(PluginError::InvalidConfigurationDescriptor {
456 plugin: descriptor.id.clone(),
457 message: format!("duplicate configuration field: {}", field.key),
458 });
459 }
460 if field.secret && field.default.is_some() {
461 return Err(PluginError::InvalidConfigurationDescriptor {
462 plugin: descriptor.id.clone(),
463 message: "secret configuration fields cannot have defaults".into(),
464 });
465 }
466 if let Some(default) = &field.default {
467 validate_configuration_value(&descriptor.id, field, default).map_err(|error| {
468 PluginError::InvalidConfigurationDescriptor {
469 plugin: descriptor.id.clone(),
470 message: error.to_string(),
471 }
472 })?;
473 }
474 }
475 Ok(())
476}
477
478fn validate_configured_descriptor(
479 base: &PluginDescriptor,
480 configured: &PluginDescriptor,
481) -> Result<(), PluginError> {
482 if configured.id != base.id
483 || configured.version != base.version
484 || configured.default_enabled != base.default_enabled
485 || configured.configuration_namespace != base.configuration_namespace
486 || configured.configuration != base.configuration
487 {
488 return Err(PluginError::ConfiguredDescriptorIdentityChanged {
489 plugin: base.id.clone(),
490 });
491 }
492 validate_configuration_descriptor(configured)?;
493
494 let core_version =
495 Version::parse(CORE_API_VERSION).map_err(|source| PluginError::InvalidCoreVersion {
496 value: CORE_API_VERSION.to_owned(),
497 source,
498 })?;
499 if !configured.core_compatibility.matches(&core_version) {
500 return Err(PluginError::IncompatibleCore {
501 plugin: configured.id.clone(),
502 requirement: configured.core_compatibility.to_string(),
503 actual: core_version,
504 });
505 }
506 Ok(())
507}
508
509fn normalize_configuration(
510 descriptor: &PluginDescriptor,
511 raw: Option<&serde_json::Value>,
512) -> Result<Option<serde_json::Value>, PluginError> {
513 if descriptor.configuration.is_empty() {
517 return Ok(raw.cloned());
518 }
519
520 let supplied = match raw {
521 None => serde_json::Map::new(),
522 Some(serde_json::Value::Object(values)) => values.clone(),
523 Some(_) => {
524 return Err(PluginError::ConfigurationMustBeObject {
525 plugin: descriptor.id.clone(),
526 });
527 }
528 };
529
530 let fields = descriptor
531 .configuration
532 .iter()
533 .map(|field| (field.key.as_str(), field))
534 .collect::<BTreeMap<_, _>>();
535
536 for key in supplied.keys() {
537 if !fields.contains_key(key.as_str()) {
538 return Err(PluginError::UnknownConfigurationField {
539 plugin: descriptor.id.clone(),
540 field: key.clone(),
541 });
542 }
543 }
544
545 let mut normalized = serde_json::Map::new();
546 for field in &descriptor.configuration {
547 let value = supplied
548 .get(&field.key)
549 .cloned()
550 .filter(|value| field.required || !value.is_null())
551 .or_else(|| field.default.clone());
552 match value {
553 Some(value) => {
554 validate_configuration_value(&descriptor.id, field, &value)?;
555 normalized.insert(field.key.clone(), value);
556 }
557 None if field.required => {
558 return Err(PluginError::MissingConfigurationField {
559 plugin: descriptor.id.clone(),
560 field: field.key.clone(),
561 });
562 }
563 None => {}
564 }
565 }
566
567 Ok(Some(serde_json::Value::Object(normalized)))
568}
569
570fn validate_configuration_value(
571 plugin: &PluginId,
572 field: &ConfigurationField,
573 value: &serde_json::Value,
574) -> Result<(), PluginError> {
575 let matches = match field.kind {
576 ConfigurationValueKind::String => value.is_string(),
577 ConfigurationValueKind::Integer => value.as_i64().is_some() || value.as_u64().is_some(),
578 ConfigurationValueKind::Number => value.is_number(),
579 ConfigurationValueKind::Boolean => value.is_boolean(),
580 ConfigurationValueKind::StringList => value
581 .as_array()
582 .is_some_and(|values| values.iter().all(serde_json::Value::is_string)),
583 ConfigurationValueKind::Object => value.is_object(),
584 };
585 if matches {
586 Ok(())
587 } else {
588 Err(PluginError::ConfigurationTypeMismatch {
589 plugin: plugin.clone(),
590 field: field.key.clone(),
591 expected: field.kind,
592 })
593 }
594}
595
596fn topological_order(
597 plugins: &BTreeMap<PluginId, EffectivePlugin>,
598) -> Result<Vec<PluginId>, PluginError> {
599 let mut visiting = BTreeSet::new();
600 let mut visited = BTreeSet::new();
601 let mut ordered = Vec::new();
602 for id in plugins.keys() {
603 visit(id, plugins, &mut visiting, &mut visited, &mut ordered)?;
604 }
605 Ok(ordered)
606}
607
608fn visit(
609 id: &PluginId,
610 plugins: &BTreeMap<PluginId, EffectivePlugin>,
611 visiting: &mut BTreeSet<PluginId>,
612 visited: &mut BTreeSet<PluginId>,
613 ordered: &mut Vec<PluginId>,
614) -> Result<(), PluginError> {
615 if visited.contains(id) {
616 return Ok(());
617 }
618 if !visiting.insert(id.clone()) {
619 return Err(PluginError::DependencyCycle(id.clone()));
620 }
621 let registration = plugins
622 .get(id)
623 .ok_or_else(|| PluginError::UnknownPlugin(id.clone()))?;
624 for dependency in ®istration.descriptor.plugin_dependencies {
625 visit(dependency, plugins, visiting, visited, ordered)?;
626 }
627 visiting.remove(id);
628 visited.insert(id.clone());
629 ordered.push(id.clone());
630 Ok(())
631}
632
633#[derive(Debug)]
634pub struct ComposedApplication {
635 pub graph: ApplicationGraph,
636 pub services: FrozenServices,
637 pub contributions: FrozenContributions,
638}
639
640#[derive(Debug, Error)]
641pub enum PluginError {
642 #[error("duplicate plugin registration: {0}")]
643 DuplicatePlugin(PluginId),
644 #[error("unknown plugin: {0}")]
645 UnknownPlugin(PluginId),
646 #[error("plugin is both explicitly enabled and disabled: {0}")]
647 ContradictorySelection(PluginId),
648 #[error("plugin {plugin} depends on unregistered plugin {dependency}")]
649 MissingPluginDependency {
650 plugin: PluginId,
651 dependency: PluginId,
652 },
653 #[error("plugin {plugin} requires disabled plugin {dependency}")]
654 DisabledRequiredPlugin {
655 plugin: PluginId,
656 dependency: PluginId,
657 },
658 #[error("plugin dependency cycle includes {0}")]
659 DependencyCycle(PluginId),
660 #[error(
661 "plugin {plugin} requires Minco core {requirement}, but this application uses {actual}"
662 )]
663 IncompatibleCore {
664 plugin: PluginId,
665 requirement: String,
666 actual: Version,
667 },
668 #[error("Minco core reported invalid version {value}: {source}")]
669 InvalidCoreVersion {
670 value: String,
671 source: semver::Error,
672 },
673 #[error("invalid configuration for plugin {plugin}: {source}")]
674 InvalidConfiguration {
675 plugin: PluginId,
676 source: serde_json::Error,
677 },
678 #[error("plugin {plugin} configuration must be a JSON object")]
679 ConfigurationMustBeObject { plugin: PluginId },
680 #[error("unknown configuration field for plugin {plugin}: {field}")]
681 UnknownConfigurationField { plugin: PluginId, field: String },
682 #[error("missing required configuration field for plugin {plugin}: {field}")]
683 MissingConfigurationField { plugin: PluginId, field: String },
684 #[error("configuration field {field} for plugin {plugin} must be {expected:?}")]
685 ConfigurationTypeMismatch {
686 plugin: PluginId,
687 field: String,
688 expected: ConfigurationValueKind,
689 },
690 #[error("invalid configuration descriptor for plugin {plugin}: {message}")]
691 InvalidConfigurationDescriptor { plugin: PluginId, message: String },
692 #[error(
693 "configured descriptor for plugin {plugin} changed immutable identity, version, selection, or configuration-schema fields"
694 )]
695 ConfiguredDescriptorIdentityChanged { plugin: PluginId },
696 #[error(transparent)]
697 Service(#[from] ServiceError),
698 #[error(transparent)]
699 Graph(#[from] GraphError),
700 #[error("plugin installation failed: {0}")]
701 Installation(String),
702}
703
704#[cfg(test)]
705mod tests {
706 use super::*;
707 use semver::{Version, VersionReq};
708
709 #[derive(Debug)]
710 struct TestPlugin {
711 descriptor: PluginDescriptor,
712 value: Option<u64>,
713 contribution: Option<String>,
714 }
715
716 impl Plugin for TestPlugin {
717 fn descriptor(&self) -> PluginDescriptor {
718 self.descriptor.clone()
719 }
720
721 fn install(&self, context: &mut PluginContext<'_>) -> Result<(), PluginError> {
722 if let Some(value) = self.value {
723 context.services().insert(Arc::new(value))?;
724 }
725 if let Some(value) = &self.contribution {
726 context.contributions().push(Arc::new(value.clone()));
727 }
728 Ok(())
729 }
730 }
731
732 fn plugin(id: &str, default_enabled: bool, value: Option<u64>) -> TestPlugin {
733 let mut descriptor =
734 PluginDescriptor::new(PluginId::new(id).unwrap(), Version::new(1, 0, 0), id);
735 descriptor.default_enabled = default_enabled;
736 TestPlugin {
737 descriptor,
738 value,
739 contribution: None,
740 }
741 }
742
743 #[test]
744 fn default_plugins_can_be_disabled() {
745 let mut manager = PluginManager::default();
746 manager.register(plugin("default", true, Some(42))).unwrap();
747 let mut selection = PluginSelection::default();
748 selection.disabled.insert(PluginId::new("default").unwrap());
749 let composed = manager.compose(&selection).unwrap();
750 assert!(composed.graph.plugins.is_empty());
751 }
752
753 #[test]
754 fn duplicate_registration_does_not_replace_the_original_plugin() {
755 let mut manager = PluginManager::default();
756 manager.register(plugin("service", true, Some(1))).unwrap();
757 assert!(matches!(
758 manager.register(plugin("service", true, Some(2))),
759 Err(PluginError::DuplicatePlugin(_))
760 ));
761 let composed = manager.compose(&PluginSelection::default()).unwrap();
762 assert_eq!(*composed.services.get::<u64>().unwrap(), 1);
763 }
764
765 #[test]
766 fn contributions_are_multi_bound_in_installation_order() {
767 let mut manager = PluginManager::default();
768 let mut first = plugin("first", true, None);
769 first.contribution = Some("one".into());
770 let mut second = plugin("second", true, None);
771 second.contribution = Some("two".into());
772 manager.register(first).unwrap();
773 manager.register(second).unwrap();
774 let composed = manager.compose(&PluginSelection::default()).unwrap();
775 let values = composed
776 .contributions
777 .get::<String>()
778 .into_iter()
779 .map(|value| (*value).clone())
780 .collect::<Vec<_>>();
781 assert_eq!(values, ["one", "two"]);
782 }
783
784 #[test]
785 fn unknown_runtime_selection_fails_closed() {
786 let manager = PluginManager::default();
787 let mut selection = PluginSelection::default();
788 selection.enabled.insert(PluginId::new("missing").unwrap());
789 assert!(matches!(
790 manager.compose(&selection),
791 Err(PluginError::UnknownPlugin(_))
792 ));
793 }
794
795 #[test]
796 fn contradictory_runtime_selection_fails_closed() {
797 let mut manager = PluginManager::default();
798 manager.register(plugin("example", false, None)).unwrap();
799 let id = PluginId::new("example").unwrap();
800 let mut selection = PluginSelection::default();
801 selection.enabled.insert(id.clone());
802 selection.disabled.insert(id);
803 assert!(matches!(
804 manager.compose(&selection),
805 Err(PluginError::ContradictorySelection(_))
806 ));
807 }
808
809 #[test]
810 fn explicit_plugin_install_exposes_typed_service() {
811 let mut manager = PluginManager::default();
812 manager
813 .register(plugin("service", false, Some(42)))
814 .unwrap();
815 let mut selection = PluginSelection::default();
816 selection.enabled.insert(PluginId::new("service").unwrap());
817 let composed = manager.compose(&selection).unwrap();
818 assert_eq!(*composed.services.get::<u64>().unwrap(), 42);
819 }
820
821 #[test]
822 fn application_services_and_contributions_can_be_injected_before_plugins_install() {
823 #[derive(Debug)]
824 struct DependsOnApplicationState;
825
826 impl Plugin for DependsOnApplicationState {
827 fn descriptor(&self) -> PluginDescriptor {
828 let mut descriptor = PluginDescriptor::new(
829 PluginId::new("depends-on-app").unwrap(),
830 Version::new(1, 0, 0),
831 "depends on composition-root state",
832 );
833 descriptor.default_enabled = true;
834 descriptor
835 }
836
837 fn install(&self, context: &mut PluginContext<'_>) -> Result<(), PluginError> {
838 let value = context.services().get::<String>()?;
839 context
840 .contributions()
841 .push(Arc::new(format!("plugin:{value}")));
842 Ok(())
843 }
844 }
845
846 let mut manager = PluginManager::default();
847 manager.register(DependsOnApplicationState).unwrap();
848 let mut services = ServiceCollection::default();
849 services.insert(Arc::new("application".to_owned())).unwrap();
850 let mut contributions = ContributionCollection::default();
851 contributions.push(Arc::new("application:base".to_owned()));
852
853 let composed = manager
854 .compose_with(&PluginSelection::default(), services, contributions)
855 .unwrap();
856 assert_eq!(
857 composed
858 .contributions
859 .get::<String>()
860 .into_iter()
861 .map(|value| (*value).clone())
862 .collect::<Vec<_>>(),
863 ["application:base", "plugin:application"]
864 );
865 }
866
867 #[test]
868 fn incompatible_core_requirement_is_rejected_during_registration() {
869 let mut incompatible = plugin("future", false, None);
870 incompatible.descriptor.core_compatibility = VersionReq::parse(">=99").unwrap();
871 assert!(matches!(
872 PluginManager::default().register(incompatible),
873 Err(PluginError::IncompatibleCore { .. })
874 ));
875 }
876
877 #[derive(Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
878 struct ExampleConfiguration {
879 message: String,
880 }
881
882 #[derive(Debug)]
883 struct ConfiguredPlugin;
884
885 impl Plugin for ConfiguredPlugin {
886 fn descriptor(&self) -> PluginDescriptor {
887 let mut descriptor = PluginDescriptor::new(
888 PluginId::new("configured").unwrap(),
889 Version::new(1, 0, 0),
890 "configured",
891 );
892 descriptor.default_enabled = true;
893 descriptor
894 }
895
896 fn install(&self, context: &mut PluginContext<'_>) -> Result<(), PluginError> {
897 let configuration = context.configuration::<ExampleConfiguration>()?;
898 context.services().insert(Arc::new(configuration))?;
899 Ok(())
900 }
901 }
902
903 #[test]
904 fn typed_plugin_configuration_is_available_during_installation() {
905 let mut manager = PluginManager::default();
906 manager.register(ConfiguredPlugin).unwrap();
907 let id = PluginId::new("configured").unwrap();
908 let mut selection = PluginSelection::default();
909 selection
910 .set_configuration(
911 id,
912 &ExampleConfiguration {
913 message: "hello".into(),
914 },
915 )
916 .unwrap();
917 let composed = manager.compose(&selection).unwrap();
918 assert_eq!(
919 composed
920 .services
921 .get::<ExampleConfiguration>()
922 .unwrap()
923 .message,
924 "hello"
925 );
926 }
927
928 #[derive(Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
929 struct SchemaConfiguration {
930 name: String,
931 enabled: bool,
932 alias: Option<String>,
933 }
934
935 #[derive(Debug)]
936 struct SchemaPlugin;
937
938 impl Plugin for SchemaPlugin {
939 fn descriptor(&self) -> PluginDescriptor {
940 let mut descriptor = PluginDescriptor::new(
941 PluginId::new("schema").unwrap(),
942 Version::new(1, 0, 0),
943 "schema",
944 );
945 descriptor.default_enabled = true;
946 descriptor.configuration.extend([
947 ConfigurationField {
948 key: "name".into(),
949 kind: ConfigurationValueKind::String,
950 required: true,
951 secret: false,
952 description: "name".into(),
953 default: None,
954 },
955 ConfigurationField {
956 key: "enabled".into(),
957 kind: ConfigurationValueKind::Boolean,
958 required: false,
959 secret: false,
960 description: "enabled".into(),
961 default: Some(serde_json::json!(true)),
962 },
963 ConfigurationField {
964 key: "alias".into(),
965 kind: ConfigurationValueKind::String,
966 required: false,
967 secret: false,
968 description: "optional alias".into(),
969 default: None,
970 },
971 ]);
972 descriptor
973 }
974
975 fn install(&self, context: &mut PluginContext<'_>) -> Result<(), PluginError> {
976 let configuration = context.configuration::<SchemaConfiguration>()?;
977 context.services().insert(Arc::new(configuration))?;
978 Ok(())
979 }
980 }
981
982 #[derive(Debug)]
983 struct ConditionalDependencyPlugin;
984
985 impl Plugin for ConditionalDependencyPlugin {
986 fn descriptor(&self) -> PluginDescriptor {
987 let mut descriptor = PluginDescriptor::new(
988 PluginId::new("conditional").unwrap(),
989 Version::new(1, 0, 0),
990 "configuration-dependent dependency",
991 );
992 descriptor.default_enabled = true;
993 descriptor.configuration.push(ConfigurationField {
994 key: "use-provider".into(),
995 kind: ConfigurationValueKind::Boolean,
996 required: false,
997 secret: false,
998 description: "enable the provider dependency".into(),
999 default: Some(serde_json::json!(false)),
1000 });
1001 descriptor
1002 }
1003
1004 fn configure_descriptor(
1005 &self,
1006 descriptor: &mut PluginDescriptor,
1007 configuration: Option<&serde_json::Value>,
1008 ) -> Result<(), PluginError> {
1009 if configuration
1010 .and_then(|value| value.get("use-provider"))
1011 .and_then(serde_json::Value::as_bool)
1012 .unwrap_or(false)
1013 {
1014 descriptor
1015 .plugin_dependencies
1016 .push(PluginId::new("provider").unwrap());
1017 descriptor.requires.push(crate::CapabilityRequirement {
1018 name: "conditional.provider".into(),
1019 version: VersionReq::parse("^1").unwrap(),
1020 });
1021 }
1022 Ok(())
1023 }
1024
1025 fn install(&self, _context: &mut PluginContext<'_>) -> Result<(), PluginError> {
1026 Ok(())
1027 }
1028 }
1029
1030 #[test]
1031 fn configured_dependencies_participate_in_resolution_and_graph_validation() {
1032 let mut provider = plugin("provider", false, None);
1033 provider
1034 .descriptor
1035 .provides
1036 .push(crate::CapabilityProvision {
1037 name: "conditional.provider".into(),
1038 version: Version::new(1, 0, 0),
1039 });
1040 let mut manager = PluginManager::default();
1041 manager.register(provider).unwrap();
1042 manager.register(ConditionalDependencyPlugin).unwrap();
1043
1044 let mut selection = PluginSelection::default();
1045 selection.configuration.insert(
1046 PluginId::new("conditional").unwrap(),
1047 serde_json::json!({"use-provider": true}),
1048 );
1049 let application = manager.compose(&selection).unwrap();
1050 let ids = application
1051 .graph
1052 .plugins
1053 .iter()
1054 .map(|plugin| plugin.id.as_str())
1055 .collect::<Vec<_>>();
1056 assert_eq!(ids, ["provider", "conditional"]);
1057 }
1058
1059 #[test]
1060 fn published_configuration_schema_applies_defaults() {
1061 let mut manager = PluginManager::default();
1062 manager.register(SchemaPlugin).unwrap();
1063 let mut selection = PluginSelection::default();
1064 selection.configuration.insert(
1065 PluginId::new("schema").unwrap(),
1066 serde_json::json!({ "name": "feedback" }),
1067 );
1068
1069 let composed = manager.compose(&selection).unwrap();
1070 assert_eq!(
1071 *composed.services.get::<SchemaConfiguration>().unwrap(),
1072 SchemaConfiguration {
1073 name: "feedback".into(),
1074 enabled: true,
1075 alias: None,
1076 }
1077 );
1078 }
1079
1080 #[test]
1081 fn typed_optional_none_is_normalized_as_an_absent_field() {
1082 let mut manager = PluginManager::default();
1083 manager.register(SchemaPlugin).unwrap();
1084 let id = PluginId::new("schema").unwrap();
1085 let mut selection = PluginSelection::default();
1086 selection
1087 .set_configuration(
1088 id,
1089 &SchemaConfiguration {
1090 name: "feedback".into(),
1091 enabled: true,
1092 alias: None,
1093 },
1094 )
1095 .unwrap();
1096
1097 let composed = manager.compose(&selection).unwrap();
1098 assert_eq!(
1099 *composed.services.get::<SchemaConfiguration>().unwrap(),
1100 SchemaConfiguration {
1101 name: "feedback".into(),
1102 enabled: true,
1103 alias: None,
1104 }
1105 );
1106 }
1107
1108 #[test]
1109 fn published_configuration_schema_rejects_unknown_missing_and_mistyped_fields() {
1110 let mut manager = PluginManager::default();
1111 manager.register(SchemaPlugin).unwrap();
1112 let id = PluginId::new("schema").unwrap();
1113
1114 for (configuration, expected) in [
1115 (
1116 serde_json::json!({ "name": "feedback", "unknown": true }),
1117 "unknown configuration field",
1118 ),
1119 (
1120 serde_json::json!({}),
1121 "missing required configuration field",
1122 ),
1123 (
1124 serde_json::json!({ "name": "feedback", "enabled": "yes" }),
1125 "must be Boolean",
1126 ),
1127 ] {
1128 let mut selection = PluginSelection::default();
1129 selection.configuration.insert(id.clone(), configuration);
1130 let error = manager.compose(&selection).unwrap_err().to_string();
1131 assert!(error.contains(expected), "{error}");
1132 }
1133 }
1134
1135 #[test]
1136 fn graph_planning_never_installs_plugin_services() {
1137 #[derive(Debug)]
1138 struct PlanningOnly;
1139
1140 impl Plugin for PlanningOnly {
1141 fn descriptor(&self) -> PluginDescriptor {
1142 let mut descriptor = PluginDescriptor::new(
1143 PluginId::new("planning-only").unwrap(),
1144 Version::new(1, 0, 0),
1145 "planning only",
1146 );
1147 descriptor.default_enabled = true;
1148 descriptor
1149 }
1150
1151 fn install(&self, _context: &mut PluginContext<'_>) -> Result<(), PluginError> {
1152 panic!("planning must not install services");
1153 }
1154 }
1155
1156 let mut manager = PluginManager::default();
1157 manager.register(PlanningOnly).unwrap();
1158
1159 let graph = manager.build_graph(&PluginSelection::default()).unwrap();
1160
1161 assert_eq!(graph.plugins[0].id.as_str(), "planning-only");
1162 }
1163
1164 #[test]
1165 fn secret_configuration_fields_cannot_publish_default_values() {
1166 #[derive(Debug)]
1167 struct UnsafeSecretDefault;
1168
1169 impl Plugin for UnsafeSecretDefault {
1170 fn descriptor(&self) -> PluginDescriptor {
1171 let mut descriptor = PluginDescriptor::new(
1172 PluginId::new("unsafe-secret").unwrap(),
1173 Version::new(1, 0, 0),
1174 "unsafe secret",
1175 );
1176 descriptor.configuration.push(ConfigurationField {
1177 key: "api_token".into(),
1178 kind: ConfigurationValueKind::String,
1179 required: true,
1180 secret: true,
1181 description: "provider API token".into(),
1182 default: Some(serde_json::json!("must-not-leak")),
1183 });
1184 descriptor
1185 }
1186
1187 fn install(&self, _context: &mut PluginContext<'_>) -> Result<(), PluginError> {
1188 Ok(())
1189 }
1190 }
1191
1192 let error = PluginManager::default()
1193 .register(UnsafeSecretDefault)
1194 .unwrap_err()
1195 .to_string();
1196
1197 assert!(error.contains("secret configuration fields cannot have defaults"));
1198 }
1199}