Skip to main content

genja_plugin_manager/
lib.rs

1#![allow(clippy::needless_doctest_main)]
2
3//! # Genja Plugin Manager
4//!
5//! A flexible and easy-to-use plugin management system for Rust applications.
6//!
7//! This crate provides dynamic loading, registration, and management of plugins at runtime.
8//! It supports individual plugins and grouped plugins, making it suitable for various
9//! application architectures where extensibility is required.
10//!
11//! ## Overview
12//!
13//! The plugin manager enables building modular applications where functionality can be
14//! added through plugins without recompilation. It handles:
15//!
16//! - Dynamic loading of plugins from shared libraries (.so, .dll, .dylib)
17//! - Plugin lifecycle management (registration, deregistration)
18//! - Type-safe plugin registry access
19//! - Metadata-driven plugin configuration
20//! - Support for multiple plugin types (Connection, Inventory, AsyncInventory,
21//!   Runner, Processor, Transform)
22//!
23//! ## Architecture
24//!
25//! ```text
26//! ┌─────────────────────────────────────────────────────────────────┐
27//! │                      PluginManager                              │
28//! │  - Loads plugins from shared libraries                          │
29//! │  - Maintains plugin registry                                    │
30//! │  - Provides type-safe access to plugins                         │
31//! └────────────────┬────────────────────────────────────────────────┘
32//!                  │
33//!                  │ manages
34//!                  │
35//!                  ▼
36//! ┌─────────────────────────────────────────────────────────────────┐
37//! │                         Plugins Enum                            │
38//! │  - Connection(Box<dyn PluginConnection>)                        │
39//! │  - Inventory(Box<dyn PluginInventory>)                          │
40//! │  - AsyncInventory(Box<dyn AsyncPluginInventory>)                │
41//! │  - Processor(Box<dyn PluginProcessor>)                          │
42//! │  - Runner(Box<dyn PluginRunner>)                                │
43//! │  - TransformFunction(Box<dyn PluginTransformFunction>)          │
44//! └─────────────────────────────────────────────────────────────────┘
45//! ```
46//!
47//! ## Quick Start
48//!
49//! ### Creating a Plugin
50//!
51//! ```rust
52//! use async_trait::async_trait;
53//! use genja_core::inventory::Hosts;
54//! use genja_core::settings::RunnerConfig;
55//! use genja_core::task::{TaskDefinition, TaskResults};
56//! use genja_plugin_manager::plugin_types::{Plugin, PluginRunner, Plugins};
57//!
58//! #[derive(Debug)]
59//! struct MyPlugin;
60//!
61//! impl Plugin for MyPlugin {
62//!     fn name(&self) -> String {
63//!         "my_plugin".to_string()
64//!     }
65//! }
66//!
67//! #[async_trait]
68//! impl PluginRunner for MyPlugin {
69//!     async fn run_task(
70//!         &self,
71//!         _task: &TaskDefinition,
72//!         _hosts: &Hosts,
73//!         _connection_resolver: Option<std::sync::Arc<dyn genja_core::task::TaskConnectionResolver>>,
74//!         _runner_config: &RunnerConfig,
75//!         _max_depth: usize,
76//!     ) -> Result<TaskResults, genja_core::GenjaError> {
77//!         // Task execution logic
78//!         Ok(TaskResults::new("my_plugin"))
79//!     }
80//!
81//!     // `run_tasks(...)` defaults to executing root task trees in order by
82//!     // delegating to `run_task(...)`. Override it only for custom batch behavior.
83//! }
84//!
85//! // Export plugin factory function
86//! #[unsafe(no_mangle)]
87//! pub fn create_plugins() -> Vec<Plugins> {
88//!     vec![Plugins::Runner(Box::new(MyPlugin))]
89//! }
90//! ```
91//!
92//! ### Using the Plugin Manager
93//!
94//! ```no_run
95//! use genja_plugin_manager::PluginManager;
96//!
97//! # fn doc_test() -> Result<(), Box<dyn std::error::Error>> {
98//! // Load plugins from a runtime plugin directory.
99//! let plugin_manager = PluginManager::new()
100//!     .load_plugins_from_directory("plugins")?;
101//!
102//! // Access plugins by type
103//! if let Some(runner) = plugin_manager.get_runner_plugin("my_plugin") {
104//!     // Use the runner plugin
105//! }
106//!
107//! // List all plugins
108//! let all_plugins = plugin_manager.get_all_plugin_names_and_groups();
109//! for (name, group) in all_plugins {
110//!     println!("Plugin: {} ({})", name, group);
111//! }
112//! # Ok(())
113//! # }
114//! ```
115//!
116//! ### Build Script Helper
117//!
118//! End-user applications that declare plugin artifacts in
119//! `[package.metadata.plugins]` can copy them into the profile-specific runtime
120//! plugin directory from `build.rs`:
121//!
122//! ```no_run
123//! fn main() {
124//!     genja_plugin_manager::build_support::copy_plugins_from_manifest().unwrap();
125//! }
126//! ```
127//!
128//! ## Plugin Configuration
129//!
130//! Plugin artifacts are declared in the `Cargo.toml` file of the end-user project
131//! using package metadata. Those entries are consumed by
132//! [`build_support::copy_plugins_from_manifest`] during the application's build,
133//! which copies the referenced shared libraries into `target/{PROFILE}/plugins`.
134//!
135//! Paths are resolved relative to the consuming application's `Cargo.toml`.
136//! That means the correct plugin path depends on whether the application is a
137//! standalone crate or a workspace member.
138//!
139//! Standalone application example:
140//!
141//! ```toml
142//! [package.metadata.plugins]
143//! my_plugin = "target/{PROFILE}/libmy_plugin.so"
144//! ```
145//!
146//! Workspace member application example:
147//!
148//! ```toml
149//! [package.metadata.plugins]
150//! my_plugin = "../target/{PROFILE}/libmy_plugin.so"
151//! ```
152//!
153//! Individual and grouped plugin entries are also supported:
154//!
155//! ```toml
156//! # Individual plugins
157//! [package.metadata.plugins]
158//! my_plugin = "target/{PROFILE}/libmy_plugin.so"
159//!
160//! # Grouped plugins
161//! [package.metadata.plugins.network]
162//! ssh = "target/{PROFILE}/libssh.so"
163//! telnet = "target/{PROFILE}/libtelnet.so"
164//!
165//! # Grouped by plugin type (recommended)
166//! [package.metadata.plugins.inventory]
167//! inventory_a = "target/{PROFILE}/libinventory.so"
168//!
169//! [package.metadata.plugins.connection]
170//! ssh = "target/{PROFILE}/libssh.so"
171//! netconf = "target/{PROFILE}/libnetconf.so"
172//!
173//! [package.metadata.plugins.runner]
174//! threaded = "target/{PROFILE}/libthreaded.so"
175//!
176//! [package.metadata.plugins.processor]
177//! audit = "target/{PROFILE}/libaudit_processor.so"
178//!
179//! [package.metadata.plugins.transform]
180//! normalize = "target/{PROFILE}/libnormalize.so"
181//! ```
182//!
183//! ## Plugin Types
184//!
185//! ### Connection Plugins
186//!
187//! Manage device connections with lifecycle hooks:
188//!
189//! ```rust
190//! use async_trait::async_trait;
191//! use genja_plugin_manager::plugin_types::{Plugin, PluginConnection};
192//! use genja_core::inventory::{ConnectionKey, ResolvedConnectionParams};
193//!
194//! #[derive(Debug)]
195//! struct SshPlugin {
196//!     key: ConnectionKey,
197//!     connected: bool,
198//! }
199//!
200//! impl Plugin for SshPlugin {
201//!     fn name(&self) -> String { "ssh".to_string() }
202//! }
203//!
204//! #[async_trait]
205//! impl PluginConnection for SshPlugin {
206//!     fn create(&self, key: &ConnectionKey) -> Box<dyn PluginConnection> {
207//!         Box::new(SshPlugin {
208//!             key: key.clone(),
209//!             connected: false,
210//!         })
211//!     }
212//!
213//!     async fn open(&mut self, params: &ResolvedConnectionParams) -> Result<(), String> {
214//!         // Establish connection
215//!         let _ = params;
216//!         self.connected = true;
217//!         Ok(())
218//!     }
219//!
220//!     fn close(&mut self) -> ConnectionKey {
221//!         self.connected = false;
222//!         self.key.clone()
223//!     }
224//!
225//!     fn is_alive(&self) -> bool {
226//!         self.connected
227//!     }
228//! }
229//! ```
230//!
231//! ### Inventory Plugins
232//!
233//! Load inventory data from various sources:
234//!
235//! ```rust
236//! use genja_plugin_manager::plugin_types::{Plugin, PluginInventory};
237//! use genja_plugin_manager::PluginManager;
238//! use genja_core::{Settings, InventoryLoadError};
239//! use genja_core::inventory::Inventory;
240//!
241//! #[derive(Debug)]
242//! struct DatabaseInventoryPlugin;
243//!
244//! impl Plugin for DatabaseInventoryPlugin {
245//!     fn name(&self) -> String { "database_inventory".to_string() }
246//! }
247//!
248//! impl PluginInventory for DatabaseInventoryPlugin {
249//!     fn load(
250//!         &self,
251//!         settings: &Settings,
252//!         plugins: &PluginManager,
253//!     ) -> Result<Inventory, InventoryLoadError> {
254//!         // Load from database
255//!         unimplemented!()
256//!     }
257//! }
258//! ```
259//!
260//! Async inventory plugins are also supported for remote inventory sources:
261//!
262//! ```rust
263//! use async_trait::async_trait;
264//! use genja_plugin_manager::plugin_types::{AsyncPluginInventory, Plugin};
265//! use genja_plugin_manager::PluginManager;
266//! use genja_core::{InventoryLoadError, Settings};
267//! use genja_core::inventory::Inventory;
268//!
269//! #[derive(Debug)]
270//! struct RemoteInventoryPlugin;
271//!
272//! impl Plugin for RemoteInventoryPlugin {
273//!     fn name(&self) -> String { "remote_inventory".to_string() }
274//! }
275//!
276//! #[async_trait]
277//! impl AsyncPluginInventory for RemoteInventoryPlugin {
278//!     async fn load_async(
279//!         &self,
280//!         settings: &Settings,
281//!         plugins: &PluginManager,
282//!     ) -> Result<Inventory, InventoryLoadError> {
283//!         let _ = (settings, plugins);
284//!         Ok(Inventory::builder().build())
285//!     }
286//! }
287//! ```
288//!
289//! ### Runner Plugins
290//!
291//! Execute tasks against hosts:
292//!
293//! ```rust
294//! use async_trait::async_trait;
295//! use genja_plugin_manager::plugin_types::{Plugin, PluginRunner};
296//! use genja_core::inventory::Hosts;
297//! use genja_core::settings::RunnerConfig;
298//! use genja_core::task::{TaskDefinition, TaskResults};
299//!
300//! #[derive(Debug)]
301//! struct ExampleSequentialRunner;
302//!
303//! impl Plugin for ExampleSequentialRunner {
304//!     // This is a custom plugin example. The built-in Genja runner name is `serial`.
305//!     fn name(&self) -> String { "example_sequential".to_string() }
306//! }
307//!
308//! #[async_trait]
309//! impl PluginRunner for ExampleSequentialRunner {
310//!     async fn run_task(
311//!         &self,
312//!         task: &TaskDefinition,
313//!         hosts: &Hosts,
314//!         connection_resolver: Option<std::sync::Arc<dyn genja_core::task::TaskConnectionResolver>>,
315//!         runner_config: &RunnerConfig,
316//!         max_depth: usize,
317//!     ) -> Result<TaskResults, genja_core::GenjaError> {
318//!         // Execute task on each host sequentially
319//!         let _ = (task, hosts, connection_resolver, runner_config, max_depth);
320//!         Ok(TaskResults::new("example_sequential"))
321//!     }
322//!
323//!     // `run_tasks(...)` defaults to executing root task trees in order by
324//!     // delegating to `run_task(...)`. Override it only for custom batch behavior.
325//! }
326//! ```
327//!
328//! ### Processor Plugins
329//!
330//! Process task lifecycle results selected by task processor names:
331//!
332//! ```rust
333//! use genja_core::task::{TaskProcessor, TaskProcessorContext, TaskResults};
334//! use genja_plugin_manager::plugin_types::{Plugin, PluginProcessor};
335//! use std::sync::Arc;
336//!
337//! #[derive(Debug)]
338//! struct AuditProcessorPlugin;
339//!
340//! impl Plugin for AuditProcessorPlugin {
341//!     fn name(&self) -> String { "audit".to_string() }
342//! }
343//!
344//! impl PluginProcessor for AuditProcessorPlugin {
345//!     fn processor(&self) -> Arc<dyn TaskProcessor> {
346//!         Arc::new(AuditProcessor)
347//!     }
348//! }
349//!
350//! struct AuditProcessor;
351//!
352//! impl TaskProcessor for AuditProcessor {
353//!     fn on_task_finish(
354//!         &self,
355//!         context: &TaskProcessorContext,
356//!         results: &mut TaskResults,
357//!     ) -> Result<(), genja_core::GenjaError> {
358//!         let _ = (context, results);
359//!         Ok(())
360//!     }
361//! }
362//! ```
363//!
364//! ### Transform Function Plugins
365//!
366//! Provide inventory transformation functions:
367//!
368//! ```rust
369//! use genja_plugin_manager::plugin_types::{Plugin, PluginTransformFunction};
370//! use genja_core::inventory::{TransformFunction, Host, BaseBuilderHost};
371//!
372//! #[derive(Debug)]
373//! struct NormalizeHostnamePlugin;
374//!
375//! impl Plugin for NormalizeHostnamePlugin {
376//!     fn name(&self) -> String { "normalize_hostname".to_string() }
377//! }
378//!
379//! impl PluginTransformFunction for NormalizeHostnamePlugin {
380//!     fn transform_function(&self) -> TransformFunction {
381//!         TransformFunction::new(|host: &Host, _options| {
382//!             if let Some(hostname) = host.hostname() {
383//!                 host.to_builder().hostname(hostname.to_lowercase()).build()
384//!             } else {
385//!                 host.clone()
386//!             }
387//!         })
388//!     }
389//! }
390//! ```
391//!
392//! ## Building Plugins
393//!
394//! ### Plugin Project Setup
395//!
396//! 1. Add dependency in `Cargo.toml`:
397//!
398//! ```toml
399//! [dependencies]
400//! genja-plugin-manager = "0.1.0"
401//! genja-core = "0.1.0"
402//! ```
403//!
404//! 2. Configure library type:
405//!
406//! ```toml
407//! [lib]
408//! name = "my_plugin"
409//! crate-type = ["lib", "cdylib"]
410//! ```
411//!
412//! 3. Build the plugin:
413//!
414//! ```bash
415//! cargo build --release
416//! ```
417//!
418//! The compiled library will be in `target/release/` with platform-specific naming:
419//! - Linux: `libmy_plugin.so`
420//! - macOS: `libmy_plugin.dylib`
421//! - Windows: `my_plugin.dll`
422//!
423//! When configuring the end-user project, prefer grouping by plugin type in
424//! `package.metadata.plugins` (see example below).
425//!
426//! ## Project Structure Differences
427//!
428//! ### Core Library (Plugin Consumer)
429//!
430//! ```toml
431//! [package]
432//! name = "genja"
433//! version = "0.1.0"
434//!
435//! [dependencies]
436//! genja-plugin-manager = "0.1.0"
437//! ```
438//!
439//! ### Plugin Project (Connection/Runner/Inventory/Processor/Transform)
440//!
441//! ```toml
442//! [package]
443//! name = "my_plugin"
444//! version = "0.1.0"
445//!
446//! [dependencies]
447//! genja-plugin-manager = "0.1.0"
448//! genja-core = "0.1.0"
449//!
450//! [lib]
451//! name = "my_plugin"
452//! crate-type = ["lib", "cdylib"]
453//! ```
454//!
455//! ### End-User Project
456//!
457//! ```toml
458//! [package]
459//! name = "genja-app"
460//! version = "0.1.0"
461//!
462//! [dependencies]
463//! genja = "0.1.0"
464//! genja-plugin-manager = "0.1.0"
465//!
466//! [build-dependencies]
467//! genja-plugin-manager = "0.1.0"
468//!
469//! # Standalone application paths.
470//! # Grouped by plugin type (recommended)
471//! [package.metadata.plugins.connection]
472//! ssh = "target/{PROFILE}/libssh.so"
473//!
474//! [package.metadata.plugins.inventory]
475//! inventory_a = "target/{PROFILE}/libinventory.so"
476//!
477//! [package.metadata.plugins.runner]
478//! threaded = "target/{PROFILE}/libthreaded.so"
479//!
480//! [package.metadata.plugins.processor]
481//! audit = "target/{PROFILE}/libaudit_processor.so"
482//!
483//! [package.metadata.plugins.transform]
484//! normalize = "target/{PROFILE}/libnormalize.so"
485//! ```
486//!
487//! If the application is a workspace member and the workspace target directory
488//! is one level up, use `../target/{PROFILE}/...` instead.
489//!
490//! Example `build.rs` for the end-user project:
491//!
492//! ```no_run
493//! fn main() {
494//!     genja_plugin_manager::build_support::copy_plugins_from_manifest().unwrap();
495//! }
496//! ```
497
498pub mod build_support;
499pub mod plugin_types;
500// pub use plugin_types;
501pub mod connection_factory;
502
503#[cfg(test)]
504use async_trait::async_trait;
505use genja_core::task::{TaskProcessor, TaskProcessorResolver};
506use libloading::{Library, Symbol};
507use plugin_types::{
508    AsyncPluginInventory, GroupOrName, PluginConnection, PluginCreatePlugins, PluginEntry,
509    PluginInventory, PluginName, PluginProcessor, PluginResultPlugins, PluginRunner,
510    PluginTransformFunction, Plugins,
511};
512use serde::Deserialize;
513use std::collections::{HashMap, hash_map};
514use std::fs;
515use std::path::{Path, PathBuf};
516use std::sync::Arc;
517// use std::error::Error;
518use std::io::{Error, ErrorKind};
519
520#[derive(Deserialize, Debug)]
521pub struct Metadata {
522    pub plugins: Option<HashMap<GroupOrName, PluginEntry>>,
523}
524
525/// Central registry and loader for dynamic plugins.
526///
527/// Holds the loaded plugin instances (`plugins`), metadata discovered from
528/// plugin manifests (`plugin_path`), and the underlying dynamic libraries
529/// (`libraries`) to keep them alive for the lifetime of the manager.
530///
531/// Note: `libraries` must be retained for as long as any plugin is in use,
532/// otherwise symbol pointers may become invalid.
533#[derive(Debug)]
534pub struct PluginManager {
535    plugins: HashMap<PluginName, Plugins>,
536    plugin_path: Vec<HashMap<GroupOrName, PluginEntry>>,
537    libraries: Vec<libloading::Library>, // Add this to keep libraries alive
538}
539
540impl Default for PluginManager {
541    fn default() -> Self {
542        Self::new()
543    }
544}
545/// Collect plugins matching a specific `Plugins` enum variant as trait objects.
546///
547/// This macro iterates the internal `plugins` map, filters by the given
548/// variant, and returns a collection of `(name, trait_object)` pairs.
549/// It is used to build typed views over the heterogeneous plugin registry.
550macro_rules! get_plugins_by_variant {
551    ($self:expr, $variant:path, $trait_type:ty) => {
552        $self
553            .plugins
554            .iter()
555            .filter_map(|(name, plugin)| match plugin {
556                $variant(inner) => Some((name, inner as $trait_type)),
557                _ => None,
558            })
559            .collect()
560    };
561}
562
563impl PluginManager {
564    pub fn new() -> Self {
565        PluginManager {
566            plugins: HashMap::new(),
567            plugin_path: Vec::new(),
568            libraries: Vec::new(),
569        }
570    }
571
572    /// Activate all plugins discovered from metadata and configured paths.
573    ///
574    /// Collects registrations from the plugin manifest and any entries in
575    /// `plugin_path`, then invokes activation for each entry. Returns the
576    /// updated `PluginManager` on success.
577    pub fn activate_plugins(mut self) -> Result<PluginManager, Box<dyn std::error::Error>> {
578        let meta_data = self.get_plugin_metadata();
579        log::debug!("Plugin metadata: {:?}", meta_data);
580        let mut registrations = Vec::new();
581        if let Some(plugin_config) = meta_data.plugins {
582            for (group_or_name, plugin_entry) in plugin_config {
583                registrations.push((group_or_name, plugin_entry));
584            }
585        } else {
586            log::error!("No plugin metadata found in manifest");
587            return Err("No plugin metadata found in manifest".into());
588        }
589        if !self.plugin_path.is_empty() {
590            for entry in &self.plugin_path {
591                for (group_or_name, plugin_entry) in entry {
592                    registrations.push((group_or_name.clone(), plugin_entry.clone()));
593                }
594            }
595        }
596        for (group_or_name, plugin_entry) in registrations {
597            self.activation_registration(group_or_name.clone(), &plugin_entry)?;
598        }
599        Ok(self)
600    }
601
602    /// Retrieves the environment variable CARGO_MANIFEST_PATH containing the
603    /// path to  manifest file. The file should contain the plugin metadata
604    /// in TOML format which contains the following structure:
605    ///
606    /// ```toml
607    /// [package.metadata.plugins]
608    /// plugin_a = "/path/to/plugin_a.so"
609    ///
610    /// [package.metadata.plugins.inventory]
611    /// inventory_plugin = "/path/to/inventory_plugin.so"
612    /// ```
613    pub fn get_plugin_metadata(&self) -> Metadata {
614        let plugin_path = std::env::var("CARGO_MANIFEST_PATH").unwrap_or_else(|_| ".".to_string());
615
616        let file_string = std::fs::read_to_string(plugin_path);
617        let manifest = match file_string {
618            Ok(manifest) => manifest,
619            Err(msg) => {
620                eprintln!("Error reading manifest file {}", msg);
621                return Metadata { plugins: None };
622            }
623        };
624        let value: toml::Value = match toml::from_str(&manifest) {
625            Ok(value) => value,
626            Err(err) => {
627                eprintln!("Error parsing manifest file: {err}");
628                return Metadata { plugins: None };
629            }
630        };
631        // let metadata = if let Some(meta_data) = value
632        if let Some(meta_data) = value
633            .get("package")
634            .and_then(|p| p.get("metadata"))
635            .and_then(|m| m.as_table())
636        {
637            let meta: Result<Metadata, toml::de::Error> =
638                toml::from_str(&toml::to_string(meta_data).unwrap());
639            meta.unwrap()
640        } else {
641            Metadata { plugins: None }
642        }
643        // metadata
644    }
645
646    /// Load and register plugins for a single manifest entry.
647    ///
648    /// Supports both individual plugin paths and grouped plugin entries.
649    /// Loaded libraries are retained to keep symbols alive.
650    fn activation_registration(
651        &mut self,
652        group_or_name: String,
653        plugin_entry: &PluginEntry,
654    ) -> Result<(), Box<dyn std::error::Error>> {
655        match plugin_entry {
656            PluginEntry::Individual(path) => {
657                log::debug!("Loading individual plugin: {group_or_name} {path}");
658                let (library, plugins) = self.load_plugin(path)?;
659                self.libraries.push(library);
660                for plugin in plugins {
661                    self.register_plugin(plugin);
662                }
663            }
664            PluginEntry::Group(group_plugins) => {
665                for (name, path) in group_plugins {
666                    log::debug!("Loading plugin group: {group_or_name}, {name} {path}");
667                    let (library, plugins) = self.load_plugin(path)?;
668                    self.libraries.push(library);
669                    for plugin in plugins {
670                        self.register_plugin(plugin);
671                    }
672                }
673            }
674        }
675        Ok(())
676    }
677
678    /// Load a dynamic library and invoke its `create_plugins` factory.
679    ///
680    /// Returns the opened library and the plugins it creates, or an error if
681    /// the file is missing, cannot be loaded, or the symbol is unavailable.
682    pub fn load_plugin(&self, filename: &str) -> PluginResultPlugins {
683        let path = Path::new(filename);
684
685        if !path.exists() {
686            let msg = format!("Plugin file does not exist: {}", filename);
687            log::error!("{msg}");
688            return Err(msg.into());
689        } else {
690            log::debug!("Attempting to load plugin: {}", filename);
691        }
692
693        let library = unsafe { Library::new(path)? };
694        log::debug!("Library loaded successfully");
695
696        let create_plugin: Symbol<PluginCreatePlugins> = unsafe { library.get(b"create_plugins")? };
697        log::debug!("Found create_plugins symbol");
698
699        let plugins = unsafe { create_plugin() };
700        log::debug!("Plugin created successfully");
701
702        Ok((library, plugins))
703    }
704
705    /// Load all plugin libraries from a directory.
706    ///
707    /// This scans the directory for files matching the current platform's
708    /// dynamic-library extension and attempts to load each one.
709    pub fn load_plugins_from_directory(
710        mut self,
711        directory: impl AsRef<Path>,
712    ) -> Result<Self, Box<dyn std::error::Error>> {
713        let directory = directory.as_ref();
714
715        if !directory.exists() {
716            return Ok(self);
717        }
718
719        if !directory.is_dir() {
720            return Err(format!("plugin path is not a directory: {}", directory.display()).into());
721        }
722
723        let extension = std::env::consts::DLL_EXTENSION;
724        let mut entries: Vec<PathBuf> = fs::read_dir(directory)?
725            .filter_map(|entry| entry.ok().map(|entry| entry.path()))
726            .filter(|path| {
727                path.is_file()
728                    && path
729                        .extension()
730                        .and_then(|value| value.to_str())
731                        .map(|value| value == extension)
732                        .unwrap_or(false)
733            })
734            .collect();
735        entries.sort();
736
737        for path in entries {
738            let filename = path
739                .to_str()
740                .ok_or_else(|| format!("path contains invalid Unicode: {}", path.display()))?;
741            let (library, plugins) = self.load_plugin(filename)?;
742            self.libraries.push(library);
743            for plugin in plugins {
744                self.register_plugin(plugin);
745            }
746        }
747
748        Ok(self)
749    }
750
751    /// Insert a plugin into the registry by name.
752    ///
753    /// Panics if a plugin with the same name is already registered.
754    pub fn register_plugin(&mut self, plugin: Plugins) {
755        let name = plugin.name();
756        log::info!("Registering plugin: {:?}", name);
757
758        if let hash_map::Entry::Vacant(entry) = self.plugins.entry(name.clone()) {
759            entry.insert(plugin);
760        } else {
761            let msg = format!("Plugin '{}' already registered", &name);
762            log::error!("{msg}");
763            panic!("{msg}");
764        }
765    }
766
767    /// Gets a plugin as a trait object based on its type
768    pub fn get_plugin(&self, name: &str) -> Option<&Plugins> {
769        self.plugins.get(name)
770    }
771
772    /// Gets an inventory plugin, returns None if the plugin is not a Base variant
773    #[allow(clippy::borrowed_box)]
774    pub fn get_connection_plugin(&self, name: &str) -> Option<&Box<dyn PluginConnection>> {
775        self.plugins.get(name).and_then(|plugin| match plugin {
776            Plugins::Connection(base) => Some(base),
777            _ => None,
778        })
779    }
780
781    #[allow(clippy::borrowed_box)]
782    /// Gets an inventory plugin, returns None if the plugin is not an Inventory variant
783    pub fn get_inventory_plugin(&self, name: &str) -> Option<&Box<dyn PluginInventory>> {
784        self.plugins.get(name).and_then(|plugin| match plugin {
785            Plugins::Inventory(inventory) => Some(inventory),
786            _ => None,
787        })
788    }
789
790    #[allow(clippy::borrowed_box)]
791    /// Gets an async inventory plugin, returns None if the plugin is not an AsyncInventory variant
792    pub fn get_async_inventory_plugin(&self, name: &str) -> Option<&Box<dyn AsyncPluginInventory>> {
793        self.plugins.get(name).and_then(|plugin| match plugin {
794            Plugins::AsyncInventory(inventory) => Some(inventory),
795            _ => None,
796        })
797    }
798
799    #[allow(clippy::borrowed_box)]
800    /// Gets a transform function plugin, returns None if the plugin is not a TransformFunction variant
801    pub fn get_transform_function_plugin(
802        &self,
803        name: &str,
804    ) -> Option<&Box<dyn PluginTransformFunction>> {
805        self.plugins.get(name).and_then(|plugin| match plugin {
806            Plugins::TransformFunction(transform) => Some(transform),
807            _ => None,
808        })
809    }
810
811    #[allow(clippy::borrowed_box)]
812    /// Gets a processor plugin, returns None if the plugin is not a Processor variant
813    pub fn get_processor_plugin(&self, name: &str) -> Option<&Box<dyn PluginProcessor>> {
814        self.plugins.get(name).and_then(|plugin| match plugin {
815            Plugins::Processor(processor) => Some(processor),
816            _ => None,
817        })
818    }
819
820    /// Generic method to get plugins by variant type with a mapper function
821    pub fn get_plugins_by_variant<'a, T>(
822        &'a self,
823        mapper: impl Fn(&'a Plugins) -> Option<T>,
824    ) -> Vec<(&'a String, T)> {
825        self.plugins
826            .iter()
827            .filter_map(|(name, plugin)| mapper(plugin).map(|p| (name, p)))
828            .collect()
829    }
830
831    // /// Gets all plugins by their type, using a mapper function to extract the desired type
832    // pub fn get_plugins_by_group<T>(&self, plugin: Plugins) -> Vec<(&String, &Box<dyn T>)> {
833    //     get_plugins_by_variant!(self, plugin, &Box<dyn T>)
834    // }
835    // pub fn get_plugins_by_group<T>(&self) -> Vec<(&String, T)> {
836    //     let mapper = |plugin| match plugin {
837    //         Plugins::Base(base) => Some(base),
838    //         _ => None,
839    //     };
840    //     let res = self.get_plugins_by_variant::<T>(mapper);
841    //     res
842    // }
843
844    /// Gets all Base plugins with their trait objects
845    #[allow(clippy::borrowed_box)]
846    pub fn get_plugins_by_type_connection(&self) -> Vec<(&String, &Box<dyn PluginConnection>)> {
847        get_plugins_by_variant!(self, Plugins::Connection, &Box<dyn PluginConnection>)
848    }
849
850    /// Gets all Inventory plugins with their trait objects
851    #[allow(clippy::borrowed_box)]
852    pub fn get_plugins_by_type_inventory(&self) -> Vec<(&String, &Box<dyn PluginInventory>)> {
853        get_plugins_by_variant!(self, Plugins::Inventory, &Box<dyn PluginInventory>)
854    }
855
856    /// Gets all async inventory plugins with their trait objects
857    #[allow(clippy::borrowed_box)]
858    pub fn get_plugins_by_type_async_inventory(
859        &self,
860    ) -> Vec<(&String, &Box<dyn AsyncPluginInventory>)> {
861        get_plugins_by_variant!(
862            self,
863            Plugins::AsyncInventory,
864            &Box<dyn AsyncPluginInventory>
865        )
866    }
867
868    /// Gets all Processor plugins with their trait objects
869    #[allow(clippy::borrowed_box)]
870    pub fn get_plugins_by_type_processor(&self) -> Vec<(&String, &Box<dyn PluginProcessor>)> {
871        get_plugins_by_variant!(self, Plugins::Processor, &Box<dyn PluginProcessor>)
872    }
873
874    /// Gets all TransformFunction plugins with their trait objects
875    #[allow(clippy::borrowed_box)]
876    pub fn get_plugins_by_type_transform_function(
877        &self,
878    ) -> Vec<(&String, &Box<dyn PluginTransformFunction>)> {
879        get_plugins_by_variant!(
880            self,
881            Plugins::TransformFunction,
882            &Box<dyn PluginTransformFunction>
883        )
884    }
885
886    /// Deregisters the plugin with the given name.
887    pub fn deregister_plugin(&mut self, name: &str) -> Option<String> {
888        if let Some(plugin) = self.plugins.remove(name) {
889            log::info!("De-registering plugin: {}", name);
890            Some(plugin.name())
891        } else {
892            None
893        }
894    }
895
896    /// Deregisters all plugins.
897    pub fn deregister_all_plugins(&mut self) -> Vec<String> {
898        let mut deregistered_plugins = Vec::new();
899        for (name, plugin) in self.plugins.drain() {
900            log::info!("De-registering plugin: {}", name);
901            deregistered_plugins.push(plugin.name());
902        }
903        deregistered_plugins
904    }
905
906    /// Merge another plugin manager into this one, overriding plugins with the same name.
907    ///
908    /// Plugin libraries and deferred plugin path entries are retained so any loaded
909    /// dynamic plugins remain valid after the merge. When a plugin name collision
910    /// occurs, the incoming plugin replaces the existing registration.
911    pub fn merge(&mut self, other: PluginManager) {
912        self.plugin_path.extend(other.plugin_path);
913        self.libraries.extend(other.libraries);
914
915        for (name, plugin) in other.plugins {
916            if self.plugins.insert(name.clone(), plugin).is_some() {
917                log::info!("Overriding plugin: {}", name);
918            } else {
919                log::info!("Registering merged plugin: {}", name);
920            }
921        }
922    }
923
924    /// Gets all the **names** of the registered plugins.
925    pub fn get_all_plugin_names(&self) -> Vec<&String> {
926        self.plugins.keys().collect()
927    }
928
929    /// Gets all the **names** and **groups** of the registered plugins.
930    pub fn get_all_plugin_names_and_groups(&self) -> Vec<(String, String)> {
931        self.plugins
932            .iter()
933            .map(|(name, plugin)| (name.clone(), plugin.group_name()))
934            .collect()
935    }
936
937    /// Gets a runner plugin by name, if registered.
938    #[allow(clippy::borrowed_box)]
939    pub fn get_runner_plugin(&self, name: &str) -> Option<&Box<dyn PluginRunner>> {
940        self.plugins.get(name).and_then(|plugin| match plugin {
941            Plugins::Runner(runner) => Some(runner),
942            _ => None,
943        })
944    }
945    pub fn with_path(mut self, path: &str, group: Option<&str>) -> Result<Self, Error> {
946        let path = Path::new(&path);
947        if path.exists() {
948            let path_string = if let Some(path_str) = path.to_str() {
949                path_str.to_string()
950            } else {
951                return Err(Error::new(
952                    ErrorKind::InvalidData,
953                    "Path contains invalid Unicode",
954                ));
955            };
956            if let Some(group_string) = group {
957                let group_info = HashMap::from([(
958                    group_string.to_string(),
959                    PluginEntry::Group(HashMap::from([(group_string.to_string(), path_string)])),
960                )]);
961                self.plugin_path.push(group_info);
962            } else {
963                let individual_info =
964                    HashMap::from([("base".to_string(), PluginEntry::Individual(path_string))]);
965                self.plugin_path.push(individual_info);
966            };
967            Ok(self)
968        } else {
969            Err(Error::new(
970                ErrorKind::NotFound,
971                format!("FileNotFoundError: {:?}", path.as_os_str()),
972            ))
973        }
974    }
975}
976
977impl TaskProcessorResolver for PluginManager {
978    fn resolve_task_processor(&self, name: &str) -> Option<Arc<dyn TaskProcessor>> {
979        self.get_processor_plugin(name)
980            .map(|processor| processor.processor())
981    }
982}
983
984#[cfg(test)]
985mod tests {
986    use std::path::{Path, PathBuf};
987    use std::process::Command;
988    use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
989    use std::time::{SystemTime, UNIX_EPOCH};
990
991    use super::*;
992    use crate::plugin_types::{
993        AsyncPluginInventory, Plugin, PluginConnection, PluginInventory, PluginProcessor,
994        PluginRunner, PluginTransformFunction,
995    };
996    use genja_core::inventory::{
997        ConnectionKey, Inventory, ResolvedConnectionParams, TransformFunction,
998    };
999    use genja_core::task::{TaskProcessor, Tasks};
1000    use genja_core::{InventoryLoadError, Settings};
1001
1002    fn env_lock() -> MutexGuard<'static, ()> {
1003        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
1004        let lock = LOCK.get_or_init(|| Mutex::new(()));
1005        lock.lock().unwrap_or_else(|err| err.into_inner())
1006    }
1007
1008    fn workspace_root() -> PathBuf {
1009        Path::new(env!("CARGO_MANIFEST_DIR"))
1010            .parent()
1011            .unwrap()
1012            .to_path_buf()
1013    }
1014
1015    fn ensure_test_plugins_built() {
1016        static BUILT: OnceLock<()> = OnceLock::new();
1017        BUILT.get_or_init(|| {
1018            let status = Command::new("cargo")
1019                .current_dir(workspace_root())
1020                .args([
1021                    "build",
1022                    "--quiet",
1023                    "-p",
1024                    "plugin-mods",
1025                    "-p",
1026                    "plugin_inventory",
1027                    "-p",
1028                    "plugin_connection",
1029                ])
1030                .status()
1031                .expect("Failed to run cargo build for test plugins");
1032            assert!(status.success(), "Failed to build test plugins");
1033        });
1034    }
1035
1036    fn set_env_var() -> MutexGuard<'static, ()> {
1037        let guard = env_lock();
1038        ensure_test_plugins_built();
1039        let file_name = match std::env::consts::OS {
1040            "linux" => "Cargo.toml",
1041            "windows" => "Cargo-windows.toml",
1042            "macos" => "Cargo-macos.toml",
1043            _ => "Cargo.toml",
1044        };
1045        let file = format!("../genja-plugin-manager/tests/plugin_mods/{}", file_name);
1046        unsafe {
1047            std::env::set_var("CARGO_MANIFEST_PATH", file);
1048        }
1049        guard
1050    }
1051
1052    fn make_file_path(module_name: &str) -> String {
1053        ensure_test_plugins_built();
1054        let mut path_name = PathBuf::new();
1055        let mut module_name_prefix = String::from(std::env::consts::DLL_PREFIX);
1056        module_name_prefix.push_str(module_name);
1057        path_name.push("..");
1058        path_name.push("target");
1059        path_name.push("debug");
1060        path_name.push(module_name_prefix);
1061        path_name.set_extension(std::env::consts::DLL_EXTENSION);
1062        path_name.to_string_lossy().to_string()
1063    }
1064
1065    fn temp_manifest_path(filename: &str) -> std::path::PathBuf {
1066        let now = SystemTime::now()
1067            .duration_since(UNIX_EPOCH)
1068            .unwrap_or_default()
1069            .as_nanos();
1070        let mut path = std::env::temp_dir();
1071        path.push(format!("genja_plugin_manager_{now}_{filename}"));
1072        path
1073    }
1074
1075    fn temp_file_path(filename: &str) -> std::path::PathBuf {
1076        let now = SystemTime::now()
1077            .duration_since(UNIX_EPOCH)
1078            .unwrap_or_default()
1079            .as_nanos();
1080        let mut path = std::env::temp_dir();
1081        path.push(format!("genja_plugin_manager_{now}_{filename}"));
1082        path
1083    }
1084
1085    #[cfg(target_os = "linux")]
1086    fn system_library_path() -> Option<&'static str> {
1087        let candidates = [
1088            "/lib/x86_64-linux-gnu/libc.so.6",
1089            "/lib64/libc.so.6",
1090            "/usr/lib/x86_64-linux-gnu/libc.so.6",
1091        ];
1092        candidates.iter().copied().find(|p| Path::new(p).exists())
1093    }
1094
1095    #[cfg(target_os = "macos")]
1096    fn system_library_path() -> Option<&'static str> {
1097        let p = "/usr/lib/libSystem.B.dylib";
1098        if Path::new(p).exists() { Some(p) } else { None }
1099    }
1100
1101    #[cfg(target_os = "windows")]
1102    fn system_library_path() -> Option<&'static str> {
1103        let p = "C:\\Windows\\System32\\kernel32.dll";
1104        if Path::new(p).exists() { Some(p) } else { None }
1105    }
1106
1107    #[test]
1108    fn get_plugin_path_test() {
1109        let _env = set_env_var();
1110        let plugin_manager = PluginManager::new();
1111        let metadata = plugin_manager.get_plugin_metadata();
1112        let plugins = metadata.plugins;
1113        match plugins {
1114            Some(plug_entry) => {
1115                for (group, entry) in plug_entry {
1116                    match entry {
1117                        PluginEntry::Individual(path) => {
1118                            assert_eq!(path, make_file_path("plugin_mods"));
1119                        }
1120                        PluginEntry::Group(path) => {
1121                            path.iter().for_each(|(metadata_name, path)| {
1122                                assert_eq!(path, &make_file_path("plugin_inventory"));
1123                                assert_eq!(metadata_name, "inventory_a");
1124                                assert_eq!(group, "inventory");
1125                            });
1126                        }
1127                    }
1128                }
1129            }
1130            None => {
1131                panic!("No plugins found in metadata");
1132            }
1133        }
1134    }
1135
1136    #[test]
1137    fn get_plugin_metadata_test() {
1138        let _env = set_env_var();
1139        let plugin_manager = PluginManager::new();
1140        let metadata = plugin_manager.get_plugin_metadata();
1141        assert!(metadata.plugins.is_some());
1142        // Check if the metadata contains the expected number of plugin paths.
1143        assert_eq!(metadata.plugins.clone().unwrap().len(), 2);
1144    }
1145
1146    #[test]
1147    fn get_plugin_metadata_missing_manifest_test() {
1148        let _env = env_lock();
1149        let missing = temp_manifest_path("missing_manifest.toml");
1150        unsafe {
1151            std::env::set_var("CARGO_MANIFEST_PATH", missing.to_string_lossy().to_string());
1152        }
1153        let plugin_manager = PluginManager::new();
1154        let metadata = plugin_manager.get_plugin_metadata();
1155        assert!(metadata.plugins.is_none());
1156    }
1157
1158    #[test]
1159    fn get_plugin_metadata_missing_metadata_section_test() {
1160        let _env = env_lock();
1161        let manifest = temp_manifest_path("no_metadata.toml");
1162        std::fs::write(
1163            &manifest,
1164            "[package]\nname = \"no_metadata\"\nversion = \"0.1.0\"\n",
1165        )
1166        .unwrap();
1167        unsafe {
1168            std::env::set_var(
1169                "CARGO_MANIFEST_PATH",
1170                manifest.to_string_lossy().to_string(),
1171            );
1172        }
1173        let plugin_manager = PluginManager::new();
1174        let metadata = plugin_manager.get_plugin_metadata();
1175        assert!(metadata.plugins.is_none());
1176        let _ = std::fs::remove_file(&manifest);
1177    }
1178
1179    #[test]
1180    fn get_plugin_metadata_invalid_toml_test() {
1181        let _env = env_lock();
1182        let manifest = temp_manifest_path("invalid_toml.toml");
1183        std::fs::write(&manifest, "[package]\nname = \"invalid\"\nversion =\n").unwrap();
1184        unsafe {
1185            std::env::set_var(
1186                "CARGO_MANIFEST_PATH",
1187                manifest.to_string_lossy().to_string(),
1188            );
1189        }
1190        let plugin_manager = PluginManager::new();
1191        let metadata = plugin_manager.get_plugin_metadata();
1192        assert!(metadata.plugins.is_none());
1193        let _ = std::fs::remove_file(&manifest);
1194    }
1195
1196    #[test]
1197    fn activate_plugins_group_invalid_path_returns_error_test() {
1198        let _env = env_lock();
1199        let manifest = temp_manifest_path("group_invalid_path.toml");
1200        std::fs::write(
1201            &manifest,
1202            r#"[package]
1203name = "invalid_group"
1204version = "0.1.0"
1205
1206[package.metadata.plugins.inventory]
1207inventory_a = "../this/path/does/not/exist.so"
1208"#,
1209        )
1210        .unwrap();
1211        unsafe {
1212            std::env::set_var(
1213                "CARGO_MANIFEST_PATH",
1214                manifest.to_string_lossy().to_string(),
1215            );
1216        }
1217        let plugin_manager = PluginManager::new();
1218        let result = plugin_manager.activate_plugins();
1219        assert!(result.is_err());
1220        let _ = std::fs::remove_file(&manifest);
1221    }
1222
1223    #[test]
1224    fn activate_plugins_test() {
1225        let _env = set_env_var();
1226        let mut plugin_manager = PluginManager::new();
1227        plugin_manager = plugin_manager.activate_plugins().unwrap();
1228        assert!(plugin_manager.get_plugin("plugin_a").is_some());
1229        assert_eq!(plugin_manager.plugins.len(), 3);
1230    }
1231
1232    #[test]
1233    #[should_panic]
1234    /// Test for duplicate activation of plugins.
1235    fn activate_plugins_and_panic_test() {
1236        let _env = set_env_var();
1237        let mut plugin_manager = PluginManager::new();
1238        plugin_manager = plugin_manager.activate_plugins().unwrap();
1239        _ = plugin_manager.activate_plugins().unwrap();
1240    }
1241
1242    #[test]
1243    fn load_plugin_test() {
1244        let plugin_manager = PluginManager::new();
1245        let filename = make_file_path("plugin_mods");
1246        let (_library, plugins) = plugin_manager.load_plugin(&filename).unwrap();
1247        assert_eq!(plugins.len(), 2);
1248        assert_eq!(plugins[0].name(), "plugin_a");
1249    }
1250
1251    #[test]
1252    fn load_plugin_and_panic_test() {
1253        let plugin_manager = PluginManager::new();
1254        let filename = make_file_path("plugin_mods");
1255        let (_library, _) = plugin_manager.load_plugin(&filename).unwrap();
1256        let filename = make_file_path("plugin_mods");
1257        let (_library, plugins) = plugin_manager.load_plugin(&filename).unwrap();
1258        assert_eq!(plugins.len(), 2);
1259        assert_eq!(plugins[0].name(), "plugin_a");
1260    }
1261
1262    #[test]
1263    fn load_plugin_missing_file_test() {
1264        let plugin_manager = PluginManager::new();
1265        let missing = temp_file_path("missing_plugin_file.so");
1266        let result = plugin_manager.load_plugin(&missing.to_string_lossy());
1267        assert!(result.is_err());
1268    }
1269
1270    #[test]
1271    fn load_plugin_invalid_library_test() {
1272        let plugin_manager = PluginManager::new();
1273        let file = temp_file_path("not_a_library.so");
1274        std::fs::write(&file, "not a library").unwrap();
1275        let result = plugin_manager.load_plugin(&file.to_string_lossy());
1276        assert!(result.is_err());
1277        let _ = std::fs::remove_file(&file);
1278    }
1279
1280    #[test]
1281    fn load_plugin_missing_symbol_test() {
1282        let plugin_manager = PluginManager::new();
1283        let Some(path) = system_library_path() else {
1284            return;
1285        };
1286        let result = plugin_manager.load_plugin(path);
1287        assert!(result.is_err());
1288    }
1289
1290    #[test]
1291    fn activate_plugins_with_groups_test() {
1292        let _env = set_env_var();
1293        let plugin_manager = PluginManager::new().activate_plugins().unwrap();
1294
1295        // Get all plugins in the "base" group
1296        let inventory_plugins = plugin_manager.get_plugins_by_type_connection();
1297        assert_eq!(inventory_plugins.len(), 2);
1298
1299        // Get all plugins in the "inventory" group
1300        let inventory_plugins = plugin_manager.get_plugins_by_type_inventory();
1301        assert_eq!(inventory_plugins.len(), 1);
1302        assert_eq!(inventory_plugins[0].1.name(), "inventory_a");
1303
1304        assert_eq!(plugin_manager.plugins.len(), 3);
1305    }
1306
1307    #[test]
1308    fn get_all_plugin_names_and_groups_test() {
1309        let _env = set_env_var();
1310        let plugin_manager = PluginManager::new().activate_plugins().unwrap();
1311        let all_plugins = plugin_manager.get_all_plugin_names_and_groups();
1312        assert_eq!(all_plugins.len(), 3);
1313        all_plugins
1314            .iter()
1315            .for_each(|(name, group)| match name.as_str() {
1316                "plugin_a" => assert_eq!(group, "Connection"),
1317                "plugin_b" => assert_eq!(group, "Connection"),
1318                "inventory_a" => assert_eq!(group, "Inventory"),
1319                _ => panic!("Unexpected plugin name"),
1320            });
1321    }
1322
1323    #[test]
1324    fn deregister_plugin_test() {
1325        let _env = set_env_var();
1326        let mut plugin_manager = PluginManager::new().activate_plugins().unwrap();
1327        assert_eq!(plugin_manager.plugins.len(), 3);
1328
1329        // Deregister individual plugin
1330        let plugin_name = plugin_manager.deregister_plugin("plugin_a");
1331        if let Some(plugin) = plugin_name {
1332            assert_eq!(plugin, "plugin_a");
1333            assert_eq!(plugin_manager.plugins.len(), 2);
1334        }
1335
1336        // Deregister grouped plugin
1337        let plugin_name = plugin_manager.deregister_plugin("inventory_a");
1338        if let Some(plugin) = plugin_name {
1339            assert_eq!(plugin, "inventory_a");
1340            assert_eq!(plugin_manager.plugins.len(), 1);
1341        }
1342
1343        // Deregister non-existent plugin
1344        let plugin_name = plugin_manager.deregister_plugin("non_existent_plugin");
1345        assert_eq!(plugin_name, None);
1346    }
1347
1348    #[test]
1349    fn deregister_all_plugins_test() {
1350        let _env = set_env_var();
1351        let mut plugin_manager = PluginManager::new().activate_plugins().unwrap();
1352        assert_eq!(plugin_manager.plugins.len(), 3);
1353
1354        // Deregister all plugins
1355        let num_plugins_deregistered = plugin_manager.deregister_all_plugins();
1356        assert_eq!(num_plugins_deregistered.len(), 3);
1357        assert_eq!(plugin_manager.plugins.len(), 0);
1358    }
1359
1360    #[test]
1361    fn plugin_manager_new_test() {
1362        let _env = set_env_var();
1363        let mut plugin_manager = PluginManager::new();
1364        assert_eq!(plugin_manager.plugins.len(), 0);
1365        plugin_manager = plugin_manager.activate_plugins().unwrap();
1366        assert_eq!(plugin_manager.plugins.len(), 3);
1367    }
1368
1369    #[test]
1370    fn get_plugins_by_type_test() {
1371        let _env = set_env_var();
1372        let plugin_manager = PluginManager::new().activate_plugins().unwrap();
1373        let connection_plugins = plugin_manager.get_plugins_by_type_connection();
1374        assert_eq!(connection_plugins.len(), 2);
1375
1376        // Check that the expected plugin names are present
1377        let base_plugin_names: Vec<&str> = connection_plugins
1378            .iter()
1379            .map(|(name, _)| name.as_str())
1380            .collect();
1381        assert!(base_plugin_names.contains(&"plugin_a"));
1382        assert!(base_plugin_names.contains(&"plugin_b"));
1383
1384        // Verify the debug output format for base plugins
1385        for (name, plugin) in connection_plugins {
1386            let debug_output = format!("{:?}", plugin);
1387            assert!(debug_output.contains("ConnectionPlugin"));
1388            assert!(debug_output.contains(name));
1389        }
1390
1391        let inventory_plugins = plugin_manager.get_plugins_by_type_inventory();
1392        assert_eq!(inventory_plugins.len(), 1);
1393    }
1394
1395    #[test]
1396    fn with_path_test() {
1397        let _env = set_env_var();
1398        let path = make_file_path("plugin_connection");
1399        let plugin_manager = PluginManager::new()
1400            .with_path(&path, None)
1401            .unwrap()
1402            .activate_plugins()
1403            .unwrap();
1404        assert_eq!(plugin_manager.plugins.len(), 4);
1405    }
1406
1407    #[test]
1408    fn with_path_group_loads_plugins() {
1409        let _env = set_env_var();
1410        let path = make_file_path("plugin_connection");
1411        let plugin_manager = PluginManager::new()
1412            .with_path(&path, Some("extra"))
1413            .unwrap()
1414            .activate_plugins()
1415            .unwrap();
1416        assert_eq!(plugin_manager.plugins.len(), 4);
1417    }
1418
1419    #[test]
1420    fn with_path_not_found_test() {
1421        let missing = temp_file_path("missing_with_path_plugin.so");
1422        let result = PluginManager::new().with_path(&missing.to_string_lossy(), None);
1423        assert!(result.is_err());
1424        if let Err(err) = result {
1425            assert_eq!(err.kind(), ErrorKind::NotFound);
1426        }
1427    }
1428
1429    #[test]
1430    #[should_panic]
1431    fn with_path_duplicate_plugin_panics_test() {
1432        let _env = set_env_var();
1433        let duplicate = make_file_path("plugin_mods");
1434        let _ = PluginManager::new()
1435            .with_path(&duplicate, None)
1436            .unwrap()
1437            .activate_plugins()
1438            .unwrap();
1439    }
1440
1441    #[derive(Debug)]
1442    struct DummyConnection {
1443        name: &'static str,
1444    }
1445
1446    impl Plugin for DummyConnection {
1447        fn name(&self) -> String {
1448            self.name.to_string()
1449        }
1450    }
1451
1452    #[async_trait]
1453    impl PluginConnection for DummyConnection {
1454        fn create(&self, _key: &ConnectionKey) -> Box<dyn PluginConnection> {
1455            Box::new(Self { name: self.name })
1456        }
1457
1458        async fn open(&mut self, _params: &ResolvedConnectionParams) -> Result<(), String> {
1459            Ok(())
1460        }
1461
1462        fn close(&mut self) -> ConnectionKey {
1463            ConnectionKey::new("dummy", "conn")
1464        }
1465
1466        fn is_alive(&self) -> bool {
1467            false
1468        }
1469    }
1470
1471    #[derive(Debug)]
1472    struct DummyInventory {
1473        name: &'static str,
1474    }
1475
1476    impl Plugin for DummyInventory {
1477        fn name(&self) -> String {
1478            self.name.to_string()
1479        }
1480    }
1481
1482    impl PluginInventory for DummyInventory {
1483        fn load(
1484            &self,
1485            _settings: &Settings,
1486            _plugins: &PluginManager,
1487        ) -> Result<Inventory, InventoryLoadError> {
1488            Ok(Inventory::builder().build())
1489        }
1490    }
1491
1492    #[derive(Debug)]
1493    struct DummyAsyncInventory {
1494        name: &'static str,
1495    }
1496
1497    impl Plugin for DummyAsyncInventory {
1498        fn name(&self) -> String {
1499            self.name.to_string()
1500        }
1501    }
1502
1503    #[async_trait]
1504    impl AsyncPluginInventory for DummyAsyncInventory {
1505        async fn load_async(
1506            &self,
1507            _settings: &Settings,
1508            _plugins: &PluginManager,
1509        ) -> Result<Inventory, InventoryLoadError> {
1510            Ok(Inventory::builder().build())
1511        }
1512    }
1513
1514    #[derive(Debug)]
1515    struct DummyRunner {
1516        name: &'static str,
1517    }
1518
1519    impl Plugin for DummyRunner {
1520        fn name(&self) -> String {
1521            self.name.to_string()
1522        }
1523    }
1524
1525    #[async_trait]
1526    impl PluginRunner for DummyRunner {
1527        async fn run_task(
1528            &self,
1529            _task: &genja_core::task::TaskDefinition,
1530            _hosts: &genja_core::inventory::Hosts,
1531            _connection_resolver: Option<
1532                std::sync::Arc<dyn genja_core::task::TaskConnectionResolver>,
1533            >,
1534            _runner_config: &genja_core::settings::RunnerConfig,
1535            _max_depth: usize,
1536        ) -> Result<genja_core::task::TaskResults, genja_core::GenjaError> {
1537            Ok(genja_core::task::TaskResults::new(self.name))
1538        }
1539
1540        async fn run_tasks(
1541            &self,
1542            _tasks: &Tasks,
1543            _hosts: &genja_core::inventory::Hosts,
1544            _connection_resolver: Option<
1545                std::sync::Arc<dyn genja_core::task::TaskConnectionResolver>,
1546            >,
1547            _runner_config: &genja_core::settings::RunnerConfig,
1548            _max_depth: usize,
1549        ) -> Result<Vec<genja_core::task::TaskResults>, genja_core::GenjaError> {
1550            Ok(Vec::new())
1551        }
1552    }
1553
1554    #[derive(Debug)]
1555    struct DummyTransform {
1556        name: &'static str,
1557    }
1558
1559    impl Plugin for DummyTransform {
1560        fn name(&self) -> String {
1561            self.name.to_string()
1562        }
1563    }
1564
1565    impl PluginTransformFunction for DummyTransform {
1566        fn transform_function(&self) -> TransformFunction {
1567            TransformFunction::new(|host, _| host.clone())
1568        }
1569    }
1570
1571    #[derive(Debug)]
1572    struct DummyProcessorPlugin {
1573        name: &'static str,
1574    }
1575
1576    impl Plugin for DummyProcessorPlugin {
1577        fn name(&self) -> String {
1578            self.name.to_string()
1579        }
1580    }
1581
1582    impl PluginProcessor for DummyProcessorPlugin {
1583        fn processor(&self) -> Arc<dyn TaskProcessor> {
1584            Arc::new(DummyProcessor)
1585        }
1586    }
1587
1588    struct DummyProcessor;
1589
1590    impl TaskProcessor for DummyProcessor {}
1591
1592    #[test]
1593    fn get_plugin_and_typed_getters_match_variants() {
1594        let mut manager = PluginManager::new();
1595        manager.register_plugin(Plugins::Connection(Box::new(DummyConnection {
1596            name: "conn",
1597        })));
1598        manager.register_plugin(Plugins::Inventory(Box::new(DummyInventory { name: "inv" })));
1599        manager.register_plugin(Plugins::AsyncInventory(Box::new(DummyAsyncInventory {
1600            name: "ainv",
1601        })));
1602        manager.register_plugin(Plugins::Runner(Box::new(DummyRunner { name: "run" })));
1603        manager.register_plugin(Plugins::TransformFunction(Box::new(DummyTransform {
1604            name: "tf",
1605        })));
1606
1607        assert!(manager.get_plugin("conn").is_some());
1608        assert!(manager.get_plugin("inv").is_some());
1609        assert!(manager.get_plugin("ainv").is_some());
1610        assert!(manager.get_plugin("run").is_some());
1611        assert!(manager.get_plugin("tf").is_some());
1612        assert!(manager.get_plugin("missing").is_none());
1613
1614        assert!(manager.get_connection_plugin("conn").is_some());
1615        assert!(manager.get_connection_plugin("inv").is_none());
1616        assert!(manager.get_connection_plugin("ainv").is_none());
1617        assert!(manager.get_connection_plugin("run").is_none());
1618        assert!(manager.get_connection_plugin("tf").is_none());
1619
1620        assert!(manager.get_inventory_plugin("inv").is_some());
1621        assert!(manager.get_inventory_plugin("conn").is_none());
1622        assert!(manager.get_inventory_plugin("ainv").is_none());
1623        assert!(manager.get_inventory_plugin("run").is_none());
1624        assert!(manager.get_inventory_plugin("tf").is_none());
1625
1626        assert!(manager.get_async_inventory_plugin("ainv").is_some());
1627        assert!(manager.get_async_inventory_plugin("inv").is_none());
1628        assert!(manager.get_async_inventory_plugin("conn").is_none());
1629
1630        assert!(manager.get_runner_plugin("run").is_some());
1631        assert!(manager.get_runner_plugin("conn").is_none());
1632        assert!(manager.get_runner_plugin("inv").is_none());
1633        assert!(manager.get_runner_plugin("ainv").is_none());
1634        assert!(manager.get_runner_plugin("tf").is_none());
1635
1636        assert!(manager.get_transform_function_plugin("tf").is_some());
1637        assert!(manager.get_transform_function_plugin("conn").is_none());
1638        assert!(manager.get_transform_function_plugin("inv").is_none());
1639        assert!(manager.get_transform_function_plugin("ainv").is_none());
1640        assert!(manager.get_transform_function_plugin("run").is_none());
1641    }
1642
1643    #[test]
1644    fn processor_plugin_getters_and_resolver_match_processor_variant() {
1645        let mut manager = PluginManager::new();
1646        manager.register_plugin(Plugins::Connection(Box::new(DummyConnection {
1647            name: "conn",
1648        })));
1649        manager.register_plugin(Plugins::Processor(Box::new(DummyProcessorPlugin {
1650            name: "audit",
1651        })));
1652
1653        assert!(manager.get_processor_plugin("audit").is_some());
1654        assert!(manager.get_processor_plugin("conn").is_none());
1655        assert!(manager.get_processor_plugin("missing").is_none());
1656
1657        let processors = manager.get_plugins_by_type_processor();
1658        assert_eq!(processors.len(), 1);
1659        assert_eq!(processors[0].0.as_str(), "audit");
1660        assert_eq!(processors[0].1.name(), "audit");
1661
1662        assert!(manager.resolve_task_processor("audit").is_some());
1663        assert!(manager.resolve_task_processor("conn").is_none());
1664        assert!(manager.resolve_task_processor("missing").is_none());
1665    }
1666
1667    #[test]
1668    #[should_panic(expected = "Plugin 'dup' already registered")]
1669    fn register_plugin_duplicate_name_panics() {
1670        let mut manager = PluginManager::new();
1671        manager.register_plugin(Plugins::Connection(Box::new(DummyConnection {
1672            name: "dup",
1673        })));
1674        manager.register_plugin(Plugins::Connection(Box::new(DummyConnection {
1675            name: "dup",
1676        })));
1677    }
1678
1679    #[test]
1680    fn get_plugins_by_type_transform_function_and_all_names() {
1681        let mut manager = PluginManager::new();
1682        manager.register_plugin(Plugins::Connection(Box::new(DummyConnection {
1683            name: "conn",
1684        })));
1685        manager.register_plugin(Plugins::Inventory(Box::new(DummyInventory { name: "inv" })));
1686        manager.register_plugin(Plugins::AsyncInventory(Box::new(DummyAsyncInventory {
1687            name: "ainv",
1688        })));
1689        manager.register_plugin(Plugins::TransformFunction(Box::new(DummyTransform {
1690            name: "tf",
1691        })));
1692
1693        let transforms = manager.get_plugins_by_type_transform_function();
1694        assert_eq!(transforms.len(), 1);
1695        assert_eq!(transforms[0].0.as_str(), "tf");
1696
1697        let async_inventory_plugins = manager.get_plugins_by_type_async_inventory();
1698        assert_eq!(async_inventory_plugins.len(), 1);
1699        assert_eq!(async_inventory_plugins[0].0.as_str(), "ainv");
1700
1701        let names = manager.get_all_plugin_names();
1702        assert_eq!(names.len(), 4);
1703        assert!(names.contains(&&"conn".to_string()));
1704        assert!(names.contains(&&"inv".to_string()));
1705        assert!(names.contains(&&"ainv".to_string()));
1706        assert!(names.contains(&&"tf".to_string()));
1707    }
1708
1709    #[test]
1710    fn merge_overrides_existing_plugins_by_name() {
1711        let mut base = PluginManager::new();
1712        base.register_plugin(Plugins::Connection(Box::new(DummyConnection {
1713            name: "conn",
1714        })));
1715
1716        let mut custom = PluginManager::new();
1717        custom.register_plugin(Plugins::Runner(Box::new(DummyRunner { name: "run" })));
1718        custom.register_plugin(Plugins::Connection(Box::new(DummyConnection {
1719            name: "conn",
1720        })));
1721
1722        base.merge(custom);
1723
1724        assert!(base.get_connection_plugin("conn").is_some());
1725        assert!(base.get_runner_plugin("run").is_some());
1726        assert_eq!(base.get_all_plugin_names().len(), 2);
1727    }
1728}