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}