Skip to main content

genja_plugin_manager/
plugin_types.rs

1//! Plugin type system and trait definitions for the plugin manager.
2//!
3//! This module defines the core plugin architecture used throughout the Genja plugin system.
4//! It provides trait definitions for different plugin types, type aliases for common patterns,
5//! and the `Plugins` enum for working with heterogeneous plugin collections.
6//!
7//! # Overview
8//!
9//! The plugin system is built around a hierarchy of traits that define different plugin
10//! capabilities:
11//!
12//! ```text
13//! ┌───────────────────────────────────────────────────────────────┐
14//! │                        Plugin (Base)                          │
15//! │                   - name() -> String                          │
16//! │                   - group() -> String                         │
17//! └───────────────────────────┬───────────────────────────────────┘
18//!                             │
19//!           ┌─────────────────┼─────────────────┬─────────────────┬─────────────────┬─────────────────┐
20//!           │                 │                 │                 │                 │                 │
21//!           ▼                 ▼                 ▼                 ▼                 ▼                 ▼    
22//! ┌─────────────────┐┌─────────────────┐┌─────────────────┐┌─────────────────┐┌─────────────────┐┌─────────────────┐
23//! │PluginConnection ││PluginInventory  ││AsyncPluginInv.  ││  PluginRunner   ││PluginTransform  ││PluginProcessor  │
24//! │                 ││                 ││                 ││                 ││    Function     ││                 │
25//! │ - create()      ││ - load()        ││ - load_async()  ││ - run_task()    ││ - transform_    ││ - processor()   │
26//! │ - open()        ││                 ││                 ││ - run_tasks()   ││   function()    ││                 │
27//! │ - close()       ││                 ││                 ││                 ││                 ││                 │
28//! │ - is_alive()    ││                 ││                 ││                 ││                 ││                 │
29//! └─────────────────┘└─────────────────┘└─────────────────┘└─────────────────┘└─────────────────┘└─────────────────┘
30//! ```
31//!
32//! # Plugin Types
33//!
34//! ## Base Plugin Trait
35//!
36//! All plugins must implement the [`Plugin`] trait, which provides:
37//! - A unique name for identification
38//! - A group classification for organizational purposes
39//!
40//! ## Specialized Plugin Traits
41//!
42//! ### [`PluginConnection`]
43//! Manages device connections with lifecycle hooks for establishing and tearing down
44//! sessions. Used for protocols like SSH, Telnet, NETCONF, etc.
45//!
46//! **Key Methods:**
47//! - `create()` - Create new connection instances per host
48//! - `open()` - Establish connection with resolved parameters
49//! - `close()` - Tear down connection and cleanup resources
50//! - `is_alive()` - Check connection health status
51//!
52//! ### [`PluginInventory`]
53//! Loads and prepares inventory data from various sources. Overrides default
54//! inventory loading behavior.
55//!
56//! **Key Methods:**
57//! - `load()` - Load inventory from source (files, APIs, databases, etc.)
58//!
59//! ### [`AsyncPluginInventory`]
60//! Loads and prepares inventory data asynchronously for remote sources such as
61//! HTTP APIs, databases, or service-discovery systems.
62//!
63//! **Key Methods:**
64//! - `load_async()` - Load inventory from an async source
65//!
66//! ### [`PluginRunner`]
67//! Executes tasks against sets of hosts. Provides different execution strategies
68//! (sequential, parallel, etc.).
69//!
70//! **Key Methods:**
71//! - `run_task()` - Execute a single task
72//! - `run_tasks()` - Execute an ordered list of root task trees
73//!
74//! ### [`PluginTransformFunction`]
75//! Provides inventory transformation functions for normalizing or modifying
76//! inventory data during loading.
77//!
78//! **Key Methods:**
79//! - `transform_function()` - Returns the transform function implementation
80//!
81//! ### [`PluginProcessor`]
82//! Provides task-result lifecycle hooks. Processor plugins are registered by
83//! name, and tasks opt into them by listing processor names on the task or with
84//! `#[genja_task(processors = ["name"])]` when using the task authoring macro.
85//!
86//! **Key Methods:**
87//! - `processor()` - Returns the task processor implementation
88//!
89//! # Type Aliases
90//!
91//! The module provides several type aliases for common patterns:
92//!
93//! - [`PathString`] - Filesystem path to a plugin library
94//! - [`GroupOrName`] - Plugin name or group identifier
95//! - [`PluginName`] - Display name for plugin identification
96//! - [`PluginResult`] - Result type for plugin loading operations
97//! - [`PluginCreate`] - Factory function signature for plugin creation
98//!
99//! # The Plugins Enum
100//!
101//! The [`Plugins`] enum provides a heterogeneous container for different plugin types,
102//! allowing them to be stored in a single collection:
103//!
104//! ```rust
105//! use genja_plugin_manager::plugin_types::Plugins;
106//!
107//! // Store different plugin types in a single vector
108//! let plugins: Vec<Plugins> = vec![
109//!     // Plugins::Connection(Box::new(ssh_plugin)),
110//!     // Plugins::Inventory(Box::new(file_plugin)),
111//!     // Plugins::AsyncInventory(Box::new(remote_inventory_plugin)),
112//!     // Plugins::Processor(Box::new(audit_processor_plugin)),
113//!     // Plugins::Runner(Box::new(threaded_runner)),
114//! ];
115//! ```
116//!
117//! # Plugin Metadata
118//!
119//! ## PluginEntry
120//!
121//! The [`PluginEntry`] enum represents plugin configuration in metadata:
122//!
123//! ```toml
124//! # Individual plugin
125//! [package.metadata.plugins]
126//! ssh_plugin = "/path/to/libssh_plugin.so"
127//!
128//! # Grouped plugins
129//! [package.metadata.plugins.connection]
130//! ssh = "/path/to/libssh.so"
131//! telnet = "/path/to/libtelnet.so"
132//!
133//! # Grouped by plugin type
134//! [package.metadata.plugins.processor]
135//! audit = "/path/to/libaudit_processor.so"
136//! ```
137//!
138//! ## PluginInfo
139//!
140//! The [`PluginInfo`] struct combines a plugin instance with its optional group:
141//!
142//! ```rust
143//! use genja_plugin_manager::plugin_types::PluginInfo;
144//!
145//! // let info = PluginInfo {
146//! //     plugin: Box::new(my_plugin),
147//! //     group: Some("network".to_string()),
148//! // };
149//! ```
150//!
151//! # Usage Examples
152//!
153//! ## Implementing a Connection Plugin
154//!
155//! ```rust
156//! use async_trait::async_trait;
157//! use genja_plugin_manager::plugin_types::{Plugin, PluginConnection};
158//! use genja_core::inventory::{ConnectionKey, ResolvedConnectionParams};
159//!
160//! #[derive(Debug)]
161//! struct SshPlugin {
162//!     key: ConnectionKey,
163//!     connected: bool,
164//! }
165//!
166//! impl Plugin for SshPlugin {
167//!     fn name(&self) -> String {
168//!         "ssh".to_string()
169//!     }
170//! }
171//!
172//! #[async_trait]
173//! impl PluginConnection for SshPlugin {
174//!     fn create(&self, key: &ConnectionKey) -> Box<dyn PluginConnection> {
175//!         Box::new(SshPlugin {
176//!             key: key.clone(),
177//!             connected: false,
178//!         })
179//!     }
180//!
181//!     async fn open(&mut self, params: &ResolvedConnectionParams) -> Result<(), String> {
182//!         // Establish SSH connection
183//!         let _ = params;
184//!         self.connected = true;
185//!         Ok(())
186//!     }
187//!
188//!     fn close(&mut self) -> ConnectionKey {
189//!         // Clean up SSH connection
190//!         self.connected = false;
191//!         self.key.clone()
192//!     }
193//!
194//!     fn is_alive(&self) -> bool {
195//!         self.connected
196//!     }
197//! }
198//! ```
199//!
200//! ## Implementing an Inventory Plugin
201//!
202//! ```rust
203//! use genja_plugin_manager::plugin_types::{Plugin, PluginInventory};
204//! use genja_plugin_manager::PluginManager;
205//! use genja_core::{Settings, InventoryLoadError};
206//! use genja_core::inventory::Inventory;
207//!
208//! #[derive(Debug)]
209//! struct DatabaseInventoryPlugin;
210//!
211//! impl Plugin for DatabaseInventoryPlugin {
212//!     fn name(&self) -> String {
213//!         "database_inventory".to_string()
214//!     }
215//! }
216//!
217//! impl PluginInventory for DatabaseInventoryPlugin {
218//!     fn load(
219//!         &self,
220//!         settings: &Settings,
221//!         plugins: &PluginManager,
222//!     ) -> Result<Inventory, InventoryLoadError> {
223//!         // Load inventory from database
224//!         // let inventory = fetch_from_database(settings)?;
225//!         // Ok(inventory)
226//!         unimplemented!()
227//!     }
228//! }
229//! ```
230//!
231//! ## Implementing an Async Inventory Plugin
232//!
233//! ```rust
234//! use async_trait::async_trait;
235//! use genja_plugin_manager::plugin_types::{AsyncPluginInventory, Plugin};
236//! use genja_plugin_manager::PluginManager;
237//! use genja_core::{InventoryLoadError, Settings};
238//! use genja_core::inventory::Inventory;
239//!
240//! #[derive(Debug)]
241//! struct RemoteInventoryPlugin;
242//!
243//! impl Plugin for RemoteInventoryPlugin {
244//!     fn name(&self) -> String {
245//!         "remote_inventory".to_string()
246//!     }
247//! }
248//!
249//! #[async_trait]
250//! impl AsyncPluginInventory for RemoteInventoryPlugin {
251//!     async fn load_async(
252//!         &self,
253//!         settings: &Settings,
254//!         plugins: &PluginManager,
255//!     ) -> Result<Inventory, InventoryLoadError> {
256//!         let _ = (settings, plugins);
257//!         Ok(Inventory::builder().build())
258//!     }
259//! }
260//! ```
261//!
262//! ## Implementing a Runner Plugin
263//!
264//! ```rust
265//! use async_trait::async_trait;
266//! use genja_plugin_manager::plugin_types::{Plugin, PluginRunner};
267//! use genja_core::inventory::Hosts;
268//! use genja_core::settings::RunnerConfig;
269//! use genja_core::task::{TaskDefinition, TaskResults};
270//!
271//! #[derive(Debug)]
272//! struct ExampleSequentialRunner;
273//!
274//! impl Plugin for ExampleSequentialRunner {
275//!     fn name(&self) -> String {
276//!         // This is a custom plugin example. The built-in Genja runner name is `serial`.
277//!         "example_sequential".to_string()
278//!     }
279//! }
280//!
281//! #[async_trait]
282//! impl PluginRunner for ExampleSequentialRunner {
283//!     async fn run_task(
284//!         &self,
285//!         task: &TaskDefinition,
286//!         hosts: &Hosts,
287//!         connection_resolver: Option<std::sync::Arc<dyn genja_core::task::TaskConnectionResolver>>,
288//!         runner_config: &RunnerConfig,
289//!         max_depth: usize,
290//!     ) -> Result<TaskResults, genja_core::GenjaError> {
291//!         // Execute task sequentially on each host
292//!         let _ = (task, hosts, connection_resolver, runner_config, max_depth);
293//!         Ok(TaskResults::new("example_sequential"))
294//!     }
295//!
296//!     // `run_tasks(...)` has a default implementation that preserves task order
297//!     // and delegates each root task tree to `run_task(...)`. Override it only when
298//!     // the runner needs custom batching behavior.
299//! }
300//! ```
301//!
302//! ## Implementing a Processor Plugin
303//!
304//! ```rust
305//! use genja_core::task::{
306//!     HostTaskResult, TaskProcessor, TaskProcessorContext, TaskResults,
307//! };
308//! use genja_plugin_manager::plugin_types::{Plugin, PluginProcessor};
309//! use std::sync::Arc;
310//!
311//! #[derive(Debug)]
312//! struct AuditProcessorPlugin;
313//!
314//! impl Plugin for AuditProcessorPlugin {
315//!     fn name(&self) -> String {
316//!         "audit".to_string()
317//!     }
318//! }
319//!
320//! impl PluginProcessor for AuditProcessorPlugin {
321//!     fn processor(&self) -> Arc<dyn TaskProcessor> {
322//!         Arc::new(AuditProcessor)
323//!     }
324//! }
325//!
326//! struct AuditProcessor;
327//!
328//! impl TaskProcessor for AuditProcessor {
329//!     fn on_task_finish(
330//!         &self,
331//!         context: &TaskProcessorContext,
332//!         results: &mut TaskResults,
333//!     ) -> Result<(), genja_core::GenjaError> {
334//!         let _ = (context, results);
335//!         Ok(())
336//!     }
337//!
338//!     fn on_instance_finish(
339//!         &self,
340//!         context: &TaskProcessorContext,
341//!         result: &mut HostTaskResult,
342//!     ) -> Result<(), genja_core::GenjaError> {
343//!         let _ = (context, result);
344//!         Ok(())
345//!     }
346//! }
347//! ```
348//!
349//! ## Implementing a Transform Function Plugin
350//!
351//! ```rust
352//! use genja_plugin_manager::plugin_types::{Plugin, PluginTransformFunction};
353//! use genja_core::inventory::{TransformFunction, Host, BaseBuilderHost};
354//!
355//! #[derive(Debug)]
356//! struct NormalizeHostnamePlugin;
357//!
358//! impl Plugin for NormalizeHostnamePlugin {
359//!     fn name(&self) -> String {
360//!         "normalize_hostname".to_string()
361//!     }
362//! }
363//!
364//! impl PluginTransformFunction for NormalizeHostnamePlugin {
365//!     fn transform_function(&self) -> TransformFunction {
366//!         TransformFunction::new(|host: &Host, _options| {
367//!             // Normalize hostname to lowercase
368//!             if let Some(hostname) = host.hostname() {
369//!                 host.to_builder().hostname(hostname.to_lowercase()).build()
370//!             } else {
371//!                 host.clone()
372//!             }
373//!         })
374//!     }
375//! }
376//! ```
377//!
378//! ## Working with the Plugins Enum
379//!
380//! ```rust
381//! use genja_plugin_manager::plugin_types::Plugins;
382//!
383//! fn process_plugin(plugin: &Plugins) {
384//!     match plugin {
385//!         Plugins::Connection(conn) => {
386//!             println!("Connection plugin: {}", conn.name());
387//!         }
388//!         Plugins::Inventory(inv) => {
389//!             println!("Inventory plugin: {}", inv.name());
390//!         }
391//!         Plugins::AsyncInventory(inv) => {
392//!             println!("Async inventory plugin: {}", inv.name());
393//!         }
394//!         Plugins::Processor(processor) => {
395//!             println!("Processor plugin: {}", processor.name());
396//!         }
397//!         Plugins::Runner(runner) => {
398//!             println!("Runner plugin: {}", runner.name());
399//!         }
400//!         Plugins::TransformFunction(tf) => {
401//!             println!("Transform function plugin: {}", tf.name());
402//!         }
403//!     }
404//! }
405//! ```
406//!
407//! # Plugin Factory Functions
408//!
409//! Plugins are created through factory functions exported from dynamic libraries:
410//!
411//! ```rust
412//! use genja_plugin_manager::plugin_types::Plugins;
413//!
414//! #[unsafe(no_mangle)]
415//! pub fn create_plugins() -> Vec<Plugins> {
416//!     vec![
417//!         // Plugins::Connection(Box::new(SshPlugin::new())),
418//!         // Plugins::Processor(Box::new(AuditProcessorPlugin)),
419//!         // Plugins::Runner(Box::new(SequentialRunner)),
420//!     ]
421//! }
422//!
423
424use async_trait::async_trait;
425use libloading::Library;
426use serde::Deserialize;
427use std::any::Any;
428use std::collections::HashMap;
429use std::fmt;
430use std::fmt::Debug;
431
432use crate::PluginManager;
433use genja_core::inventory::{
434    ConnectionKey, Hosts, Inventory, ResolvedConnectionParams, TransformFunction,
435};
436use genja_core::settings::RunnerConfig;
437use genja_core::task::{TaskConnectionResolver, TaskDefinition, TaskProcessor, TaskResults, Tasks};
438use genja_core::{InventoryLoadError, Settings};
439use std::sync::Arc;
440/// Filesystem path to a plugin or plugin metadata entry.
441pub type PathString = String;
442/// Shared alias for a group name or plugin name key.
443pub type GroupOrName = String;
444/// Display name used to identify a plugin in the registry.
445pub type PluginName = String;
446/// Result of loading a plugin library and its exported plugin instances.
447pub type PluginResult = Result<(Library, Vec<Box<dyn Plugin>>), Box<dyn std::error::Error>>;
448/// Signature for a plugin factory function exported by dynamic libraries.
449pub type PluginCreate = unsafe fn() -> Vec<Box<dyn Plugin>>;
450
451/// Signature for a plugin factory function exported by dynamic libraries.
452pub type PluginCreatePlugins = unsafe fn() -> Vec<Plugins>;
453/// Result of loading a plugin library and its exported plugin instances.
454pub type PluginResultPlugins = Result<(Library, Vec<Plugins>), Box<dyn std::error::Error>>;
455
456/// Plugin entry in metadata, either a single path or a named group of paths.
457#[derive(Deserialize, Debug, Clone)]
458#[serde(untagged)]
459pub enum PluginEntry {
460    Individual(PathString),
461    Group(HashMap<String, PathString>),
462}
463
464/// Information about a loaded plugin, including the plugin itself and its group.
465pub struct PluginInfo {
466    pub plugin: Box<dyn Plugin>,
467    pub group: Option<String>,
468}
469
470/// Base plugin interface implemented by all plugins.
471///
472/// Provides a name and an optional group label.
473pub trait Plugin: Send + Sync + Any {
474    /// The name of the plugin. This is used to identify the plugin.
475    fn name(&self) -> String;
476
477    /// Returns the group name
478    fn group(&self) -> String {
479        String::from("BasePlugin")
480    }
481}
482
483/// Loads or prepares inventory data for the system.
484///
485/// Inventory plugins override the default inventory loading behavior provided
486/// by the settings module. They provide the source of host data consumed by
487/// runners and transforms. Implementations should be safe to call from multiple
488/// threads and should avoid mutating shared state without synchronization.
489pub trait PluginInventory: Plugin {
490    /// Load and return inventory data for the system.
491    fn load(
492        &self,
493        settings: &Settings,
494        plugins: &PluginManager,
495    ) -> Result<Inventory, InventoryLoadError>;
496
497    /// Returns the group name
498    fn group(&self) -> String {
499        String::from("InventoryPlugin")
500    }
501}
502
503/// Loads or prepares inventory data for the system asynchronously.
504///
505/// Async inventory plugins are intended for remote inventory sources such as
506/// HTTP APIs, databases, or service-discovery systems. The runtime can prefer
507/// them from async construction paths while keeping synchronous inventory
508/// plugins available for file-based and blocking implementations.
509#[async_trait]
510pub trait AsyncPluginInventory: Plugin {
511    /// Load and return inventory data for the system.
512    async fn load_async(
513        &self,
514        settings: &Settings,
515        plugins: &PluginManager,
516    ) -> Result<Inventory, InventoryLoadError>;
517
518    /// Returns the group name
519    fn group(&self) -> String {
520        String::from("InventoryPlugin")
521    }
522}
523
524impl Debug for dyn Plugin {
525    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
526        write!(f, "{} {{ name: {} }}", Plugin::group(self), self.name())
527    }
528}
529
530impl Debug for dyn PluginInventory {
531    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
532        write!(
533            f,
534            "{} {{ name: {} }}",
535            PluginInventory::group(self),
536            self.name()
537        )
538    }
539}
540
541impl Debug for dyn AsyncPluginInventory {
542    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
543        write!(
544            f,
545            "{} {{ name: {} }}",
546            AsyncPluginInventory::group(self),
547            self.name()
548        )
549    }
550}
551
552impl Debug for dyn PluginConnection {
553    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
554        write!(
555            f,
556            "{} {{ name: {} }}",
557            PluginConnection::group(self),
558            self.name()
559        )
560    }
561}
562
563/// Executes tasks against a set of hosts.
564///
565/// Runner plugins provide task execution for a given inventory and task list.
566/// Implementers should be safe to call from multiple threads and should avoid
567/// mutating shared state without synchronization.
568#[async_trait]
569pub trait PluginRunner: Plugin {
570    /// Run a single task against the provided hosts.
571    async fn run_task(
572        &self,
573        task: &TaskDefinition,
574        hosts: &Hosts,
575        connection_resolver: Option<Arc<dyn TaskConnectionResolver>>,
576        runner_config: &RunnerConfig,
577        max_depth: usize,
578    ) -> Result<TaskResults, genja_core::GenjaError>;
579
580    /// Run all tasks in the provided task list against the provided hosts.
581    ///
582    /// The default implementation preserves the order of `tasks`, executing each
583    /// root task tree by delegating to [`Self::run_task`]. Runners can override this
584    /// when they need custom batching behavior.
585    async fn run_tasks(
586        &self,
587        tasks: &Tasks,
588        hosts: &Hosts,
589        connection_resolver: Option<Arc<dyn TaskConnectionResolver>>,
590        runner_config: &RunnerConfig,
591        max_depth: usize,
592    ) -> Result<Vec<TaskResults>, genja_core::GenjaError> {
593        let mut results = Vec::with_capacity(tasks.len());
594        for task in tasks.iter() {
595            results.push(
596                self.run_task(
597                    task,
598                    hosts,
599                    connection_resolver.clone(),
600                    runner_config,
601                    max_depth,
602                )
603                .await?,
604            );
605        }
606        Ok(results)
607    }
608
609    /// Returns the group name
610    fn group(&self) -> String {
611        String::from("RunnerPlugin")
612    }
613}
614
615impl Debug for dyn PluginRunner {
616    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
617        write!(
618            f,
619            "{} {{ name: {} }}",
620            PluginRunner::group(self),
621            self.name()
622        )
623    }
624}
625
626/// Provides an inventory transform function.
627///
628/// Transform-function plugins supply a `TransformFunction` used to modify or
629/// normalize inventory data during loading.
630pub trait PluginTransformFunction: Plugin {
631    /// Returns a transform function instance for inventory processing.
632    fn transform_function(&self) -> TransformFunction;
633
634    /// Returns the group name
635    fn group(&self) -> String {
636        String::from("TransformFunctionPlugin")
637    }
638}
639
640impl Debug for dyn PluginTransformFunction {
641    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
642        write!(
643            f,
644            "{} {{ name: {} }}",
645            PluginTransformFunction::group(self),
646            self.name()
647        )
648    }
649}
650
651/// Provides task-result processing hooks.
652///
653/// Processor plugins are registered by name and made available before runner
654/// execution. Each task selects the processor names it wants. Sub-tasks select
655/// their own processors, which keeps deeply nested task behavior explicit.
656pub trait PluginProcessor: Plugin {
657    /// Returns the processor implementation used during task execution.
658    fn processor(&self) -> Arc<dyn TaskProcessor>;
659
660    /// Returns the group name
661    fn group(&self) -> String {
662        String::from("ProcessorPlugin")
663    }
664}
665
666impl Debug for dyn PluginProcessor {
667    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
668        write!(
669            f,
670            "{} {{ name: {} }}",
671            PluginProcessor::group(self),
672            self.name()
673        )
674    }
675}
676
677/// Manages device connections for plugins that need an explicit session.
678///
679/// Connection plugins provide lifecycle hooks for establishing and tearing down
680/// connections and expose a connection operation for downstream use.
681#[async_trait]
682pub trait PluginConnection: Plugin {
683    /// Create a new per-host connection instance.
684    fn create(&self, key: &ConnectionKey) -> Box<dyn PluginConnection>;
685
686    /// Open a connection to a device.
687    async fn open(&mut self, params: &ResolvedConnectionParams) -> Result<(), String>;
688
689    async fn execute_command(&mut self, _command: &str) -> Result<String, String> {
690        Err("connection plugin does not implement execute_command".to_string())
691    }
692
693    /// Close a connection to a device.
694    fn close(&mut self) -> ConnectionKey;
695
696    /// Returns `true` if the connection is alive.
697    fn is_alive(&self) -> bool;
698
699    /// Returns the group name
700    fn group(&self) -> String {
701        String::from("ConnectionPlugin")
702    }
703}
704
705/// Heterogeneous container for supported plugin trait objects.
706///
707/// Each variant wraps a boxed trait object that implements a specific plugin
708/// interface.
709#[derive(Debug)]
710pub enum Plugins {
711    Connection(Box<dyn PluginConnection>),
712    Inventory(Box<dyn PluginInventory>),
713    AsyncInventory(Box<dyn AsyncPluginInventory>),
714    Processor(Box<dyn PluginProcessor>),
715    Runner(Box<dyn PluginRunner>),
716    TransformFunction(Box<dyn PluginTransformFunction>),
717}
718
719impl Plugins {
720    /// Return the plugin's declared name.
721    pub fn name(&self) -> String {
722        match self {
723            Plugins::Connection(connection) => connection.name(),
724            Plugins::Inventory(inventory) => inventory.name(),
725            Plugins::AsyncInventory(inventory) => inventory.name(),
726            Plugins::Processor(processor) => processor.name(),
727            Plugins::Runner(runner) => runner.name(),
728            Plugins::TransformFunction(transform) => transform.name(),
729        }
730    }
731
732    /// Return the logical group name for this plugin variant.
733    pub fn group_name(&self) -> String {
734        match self {
735            Plugins::Connection(_) => String::from("Connection"),
736            Plugins::Inventory(_) => String::from("Inventory"),
737            Plugins::AsyncInventory(_) => String::from("Inventory"),
738            Plugins::Processor(_) => String::from("Processor"),
739            Plugins::Runner(_) => String::from("Runner"),
740            Plugins::TransformFunction(_) => String::from("TransformFunction"),
741        }
742    }
743}
744
745#[cfg(test)]
746mod tests {
747    use super::*;
748    use genja_core::inventory::{
749        ConnectionKey, Host, Hosts, ResolvedConnectionParams, TransformFunction,
750    };
751    use genja_core::task::{
752        HostTaskResult, Task, TaskError, TaskExecutionMode, TaskInfo, TaskRuntimeContext,
753        TaskSuccess,
754    };
755    use serde_json::{Value, json};
756    use std::future::Future;
757    use tokio::runtime::Builder;
758
759    #[derive(Debug)]
760    struct DummyPlugin {
761        name: &'static str,
762    }
763
764    impl DummyPlugin {
765        fn new(name: &'static str) -> Self {
766            Self { name }
767        }
768    }
769
770    impl Plugin for DummyPlugin {
771        fn name(&self) -> String {
772            self.name.to_string()
773        }
774    }
775
776    #[derive(Debug)]
777    struct DummyInventory {
778        name: &'static str,
779    }
780
781    impl DummyInventory {
782        fn new(name: &'static str) -> Self {
783            Self { name }
784        }
785    }
786
787    impl Plugin for DummyInventory {
788        fn name(&self) -> String {
789            self.name.to_string()
790        }
791    }
792
793    impl PluginInventory for DummyInventory {
794        fn load(
795            &self,
796            _settings: &Settings,
797            _plugins: &PluginManager,
798        ) -> Result<Inventory, InventoryLoadError> {
799            Ok(Inventory::builder().build())
800        }
801    }
802
803    #[derive(Debug)]
804    struct DummyAsyncInventory {
805        name: &'static str,
806    }
807
808    impl DummyAsyncInventory {
809        fn new(name: &'static str) -> Self {
810            Self { name }
811        }
812    }
813
814    impl Plugin for DummyAsyncInventory {
815        fn name(&self) -> String {
816            self.name.to_string()
817        }
818    }
819
820    #[async_trait]
821    impl AsyncPluginInventory for DummyAsyncInventory {
822        async fn load_async(
823            &self,
824            _settings: &Settings,
825            _plugins: &PluginManager,
826        ) -> Result<Inventory, InventoryLoadError> {
827            Ok(Inventory::builder().build())
828        }
829    }
830
831    #[derive(Debug)]
832    struct DummyRunner {
833        name: &'static str,
834    }
835
836    impl DummyRunner {
837        fn new(name: &'static str) -> Self {
838            Self { name }
839        }
840    }
841
842    impl Plugin for DummyRunner {
843        fn name(&self) -> String {
844            self.name.to_string()
845        }
846    }
847
848    #[async_trait]
849    impl PluginRunner for DummyRunner {
850        async fn run_task(
851            &self,
852            task: &TaskDefinition,
853            _hosts: &Hosts,
854            _connection_resolver: Option<Arc<dyn TaskConnectionResolver>>,
855            _runner_config: &RunnerConfig,
856            _max_depth: usize,
857        ) -> Result<TaskResults, genja_core::GenjaError> {
858            Ok(TaskResults::new(task.name()))
859        }
860    }
861
862    struct DummyTask {
863        name: &'static str,
864    }
865
866    impl TaskInfo for DummyTask {
867        fn name(&self) -> &str {
868            self.name
869        }
870
871        fn connection_plugin_name(&self) -> Option<&str> {
872            None
873        }
874
875        fn options(&self) -> Option<&Value> {
876            None
877        }
878    }
879
880    #[async_trait]
881    impl Task for DummyTask {
882        async fn start_async(
883            &self,
884            _host: &Host,
885            _context: &TaskRuntimeContext,
886        ) -> Result<HostTaskResult, TaskError> {
887            Ok(HostTaskResult::passed(TaskSuccess::new()))
888        }
889
890        fn execution_mode(&self) -> TaskExecutionMode {
891            TaskExecutionMode::Async
892        }
893    }
894
895    fn run_async<F: Future>(future: F) -> F::Output {
896        Builder::new_current_thread()
897            .enable_all()
898            .build()
899            .expect("test runtime should build")
900            .block_on(future)
901    }
902
903    #[derive(Debug)]
904    struct DummyTransform {
905        name: &'static str,
906    }
907
908    impl DummyTransform {
909        fn new(name: &'static str) -> Self {
910            Self { name }
911        }
912    }
913
914    impl Plugin for DummyTransform {
915        fn name(&self) -> String {
916            self.name.to_string()
917        }
918    }
919
920    impl PluginTransformFunction for DummyTransform {
921        fn transform_function(&self) -> TransformFunction {
922            TransformFunction::new(|host, _| host.clone())
923        }
924    }
925
926    #[derive(Debug)]
927    struct DummyConnection {
928        name: &'static str,
929        key: ConnectionKey,
930        alive: bool,
931    }
932
933    impl DummyConnection {
934        fn new(name: &'static str) -> Self {
935            Self {
936                name,
937                key: ConnectionKey::new("host1", "dummy"),
938                alive: false,
939            }
940        }
941    }
942
943    impl Plugin for DummyConnection {
944        fn name(&self) -> String {
945            self.name.to_string()
946        }
947    }
948
949    #[async_trait]
950    impl PluginConnection for DummyConnection {
951        fn create(&self, key: &ConnectionKey) -> Box<dyn PluginConnection> {
952            Box::new(Self {
953                name: self.name,
954                key: key.clone(),
955                alive: false,
956            })
957        }
958
959        async fn open(&mut self, _params: &ResolvedConnectionParams) -> Result<(), String> {
960            self.alive = true;
961            Ok(())
962        }
963
964        fn close(&mut self) -> ConnectionKey {
965            self.alive = false;
966            self.key.clone()
967        }
968
969        fn is_alive(&self) -> bool {
970            self.alive
971        }
972    }
973
974    #[test]
975    fn plugin_entry_deserializes_individual_and_group() {
976        let individual: PluginEntry = serde_json::from_value(json!("path/to/lib.so")).unwrap();
977        match individual {
978            PluginEntry::Individual(path) => assert_eq!(path, "path/to/lib.so"),
979            PluginEntry::Group(_) => panic!("expected individual plugin entry"),
980        }
981
982        let grouped: PluginEntry = serde_json::from_value(json!({
983            "ssh": "path/to/libssh.so",
984            "telnet": "path/to/libtelnet.so"
985        }))
986        .unwrap();
987
988        match grouped {
989            PluginEntry::Group(map) => {
990                assert_eq!(map.get("ssh"), Some(&"path/to/libssh.so".to_string()));
991                assert_eq!(map.get("telnet"), Some(&"path/to/libtelnet.so".to_string()));
992            }
993            PluginEntry::Individual(_) => panic!("expected grouped plugin entry"),
994        }
995    }
996
997    #[test]
998    fn plugins_name_and_group_name_match_variants() {
999        let connection = Plugins::Connection(Box::new(DummyConnection::new("conn")));
1000        let inventory = Plugins::Inventory(Box::new(DummyInventory::new("inv")));
1001        let async_inventory = Plugins::AsyncInventory(Box::new(DummyAsyncInventory::new("ainv")));
1002        let runner = Plugins::Runner(Box::new(DummyRunner::new("run")));
1003        let transform = Plugins::TransformFunction(Box::new(DummyTransform::new("tf")));
1004
1005        assert_eq!(connection.name(), "conn");
1006        assert_eq!(connection.group_name(), "Connection");
1007
1008        assert_eq!(inventory.name(), "inv");
1009        assert_eq!(inventory.group_name(), "Inventory");
1010
1011        assert_eq!(async_inventory.name(), "ainv");
1012        assert_eq!(async_inventory.group_name(), "Inventory");
1013
1014        assert_eq!(runner.name(), "run");
1015        assert_eq!(runner.group_name(), "Runner");
1016
1017        assert_eq!(transform.name(), "tf");
1018        assert_eq!(transform.group_name(), "TransformFunction");
1019    }
1020
1021    #[test]
1022    fn debug_impls_include_group_and_name() {
1023        let base = DummyPlugin::new("base");
1024        let inventory = DummyInventory::new("inv");
1025        let runner = DummyRunner::new("run");
1026        let transform = DummyTransform::new("tf");
1027        let connection = DummyConnection::new("conn");
1028
1029        let base_dbg = format!("{:?}", &base as &dyn Plugin);
1030        let inventory_dbg = format!("{:?}", &inventory as &dyn PluginInventory);
1031        let runner_dbg = format!("{:?}", &runner as &dyn PluginRunner);
1032        let transform_dbg = format!("{:?}", &transform as &dyn PluginTransformFunction);
1033        let connection_dbg = format!("{:?}", &connection as &dyn PluginConnection);
1034
1035        assert_eq!(base_dbg, "BasePlugin { name: base }");
1036        assert_eq!(inventory_dbg, "InventoryPlugin { name: inv }");
1037        assert_eq!(runner_dbg, "RunnerPlugin { name: run }");
1038        assert_eq!(transform_dbg, "TransformFunctionPlugin { name: tf }");
1039        assert_eq!(connection_dbg, "ConnectionPlugin { name: conn }");
1040    }
1041
1042    #[test]
1043    fn runner_default_run_tasks_delegates_to_run_task_in_task_order() {
1044        let runner = DummyRunner::new("run");
1045        let mut tasks = Tasks::new();
1046        tasks.add_task(DummyTask { name: "first" });
1047        tasks.add_task(DummyTask { name: "second" });
1048
1049        let results =
1050            run_async(runner.run_tasks(&tasks, &Hosts::new(), None, &RunnerConfig::default(), 0))
1051                .expect("default run_tasks should execute");
1052
1053        assert_eq!(results.len(), 2);
1054        assert_eq!(results[0].task_name(), "first");
1055        assert_eq!(results[1].task_name(), "second");
1056    }
1057
1058    #[test]
1059    fn plugin_info_holds_group_and_plugin() {
1060        let info = PluginInfo {
1061            plugin: Box::new(DummyPlugin::new("example")),
1062            group: Some("network".to_string()),
1063        };
1064
1065        assert_eq!(info.plugin.name(), "example");
1066        assert_eq!(info.group.as_deref(), Some("network"));
1067    }
1068
1069    #[test]
1070    fn plugin_entry_rejects_invalid_shapes() {
1071        let bad_individual: Result<PluginEntry, _> = serde_json::from_value(serde_json::json!(123));
1072        assert!(bad_individual.is_err());
1073
1074        let bad_group: Result<PluginEntry, _> = serde_json::from_value(serde_json::json!({
1075            "ssh": 123,
1076            "telnet": false
1077        }));
1078        assert!(bad_group.is_err());
1079    }
1080}