use crate::finalize::RawPluginMap; use crate::finalize::constant::{FINALIZE_PLUGIN_MANIFEST, FINALIZE_PLUGIN_MANIFEST_EXTENSION}; use crate::finalize::plugin::metadata::{PluginMetadata, PluginType}; use crate::{EventSender, ExecutionContext, finalize::error::PluginError}; use async_trait::async_trait; use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::sync::Arc; mod dylib; pub mod fetch; mod internal; mod metadata; mod rhai; mod shell; #[derive(Clone)] pub struct FinalizePlugin { pub metadata: PluginMetadata, argument: serde_yaml::Value, pub entry: Arc, } impl FinalizePlugin { pub fn new(entry: Box) -> Self { Self { metadata: PluginMetadata::default(), argument: serde_yaml::Value::Mapping(Default::default()), entry: Arc::from(entry), } } pub fn with_metadata(mut self, metadata: PluginMetadata) -> Self { self.metadata = metadata; self } pub fn with_argument(mut self, argument: serde_yaml::Value) -> Self { self.argument = argument; self } pub async fn call( &self, runtime_argument: serde_yaml::Value, ctx: &ExecutionContext, event_tx: &EventSender, ) -> PluginResult { // shallow merge argument let _argument = match (&self.argument, runtime_argument) { (serde_yaml::Value::Mapping(base), serde_yaml::Value::Mapping(runtime)) => { let mut merged = base.clone(); for (k, v) in runtime { merged.insert(k.clone(), v.clone()); } serde_yaml::Value::Mapping(merged) } _ => self.argument.clone(), // If not both are mappings, just use the original argument }; self.entry .call(&self.metadata, _argument, ctx, event_tx) .await } } pub type PluginMap = HashMap; type PluginResult = Result<(), PluginError>; #[async_trait] pub trait AsyncPluginFn: Send + Sync { async fn call( &self, metadata: &PluginMetadata, argument: serde_yaml::Value, ctx: &ExecutionContext, event_tx: &EventSender, ) -> PluginResult; } /// Reads and parses a plugin's manifest file. /// /// # Arguments /// /// * `path` - Optional filesystem path to the plugin directory /// * `name` - Name of the plugin (used for error reporting) /// /// # Returns /// /// Returns parsed [`PluginMetadata`] on success, or a [`PluginError`] if the manifest /// cannot be read or parsed. fn read_manifest(path: Option, name: &str) -> Result { let path = path.ok_or_else(|| PluginError::PluginUnavailable { name: name.to_string(), reason: "Plugin not present in filesystem.".to_string(), })?; // Extract content from manifest.yml or manifest.yaml let manifest_content = 'load: { for ext in FINALIZE_PLUGIN_MANIFEST_EXTENSION { match load_manifest(&path, name, ext) { Ok(content) => break 'load content, Err(PluginError::ManifestNotFound(_)) => {} Err(e) => return Err(e), } } return Err(PluginError::ManifestNotFound(path)); }; log::debug!("[plugin:{}] Parsing manifest content", name); let mut metadata: PluginMetadata = serde_yaml::from_str(manifest_content.as_str()).map_err(|e| { PluginError::PluginUnavailable { name: name.to_string(), reason: format!("Failed to parse plugin manifest: {}", e), } })?; metadata.fspath = path; Ok(metadata) } fn load_manifest(path: &Path, name: &str, suffix: &str) -> Result { let manifest_path = path.join(format!("{}{}", FINALIZE_PLUGIN_MANIFEST, suffix)); log::debug!( "[plugin:{}] Reading manifest from {}", name, manifest_path.display() ); if !manifest_path.exists() { return Err(PluginError::ManifestNotFound(manifest_path)); } std::fs::read_to_string(&manifest_path).map_err(|e| PluginError::PluginUnavailable { name: name.to_string(), reason: format!( "Failed to read plugin manifest from {}: {}", manifest_path.display(), e ), }) } /// Determines the plugin type based on the manifest type or file extension. /// /// The function first returns `original` if it's not [`PluginType::Unknown`]. /// Otherwise, it infers the type from the entrypoint file extension: /// - `.sh` / `.bash` → [`PluginType::Shell`] /// - `.rhai` → [`PluginType::Rhai`] /// - `.so` / `.dll` / `.dylib` → [`PluginType::Dylib`] /// /// # Arguments /// /// * `original` - Plugin type as specified in the manifest /// * `entrypoint` - Path or filename of the plugin entrypoint /// /// # Returns /// /// The inferred [`PluginType`]. fn get_plugin_type(original: PluginType, entrypoint: &str) -> PluginType { if original != PluginType::Unknown { return original; } let _suffix = entrypoint.rsplit('.').next().unwrap_or(""); match _suffix { "sh" | "bash" => PluginType::Shell, "rhai" => PluginType::Rhai, "so" | "dll" | "dylib" => PluginType::Dylib, _ => PluginType::Unknown, } } /// Creates a plugin entry function instance based on the plugin type. /// /// # Arguments /// /// * `plugin_type` - The type of plugin to instantiate /// * `name` - Name of the plugin (used for error reporting) /// /// # Returns /// /// Returns a boxed [`AsyncPluginFn`] on success, or a [`PluginError`] if the plugin type /// is [`PluginType::Unknown`] and cannot be inferred. fn get_entry(plugin_type: PluginType, name: &str) -> Result, PluginError> { match plugin_type { PluginType::Shell => Ok(Box::new(shell::Shell)), PluginType::Rhai => Ok(Box::new(rhai::Rhai)), PluginType::Dylib => Ok(Box::new(dylib::Dylib)), PluginType::Unknown => { log::error!( "Plugin {} has unknown plugin type and cannot be inferred from entrypoint.", name ); Err(PluginError::PluginUnavailable { name: name.to_string(), reason: "Unknown plugin type and cannot be inferred from entrypoint".to_string(), }) } } } /// Registers all available plugins (internal and external) into a plugin map. /// /// Internal plugins are registered first, followed by external plugins loaded from /// the provided [`RawPluginMap`]. Plugins that are unavailable (e.g., missing runtime) /// are logged and skipped. /// /// # Arguments /// /// * `external_plugins` - Map of external plugin configurations /// /// # Returns /// /// Returns a [`PluginMap`] containing all registered plugins, or a [`PluginError`] if /// a plugin manifest cannot be read or parsed. pub fn register( external_plugins: RawPluginMap, list: Option<&HashSet>, ) -> Result { let mut plugins: PluginMap = HashMap::new(); let mut count = 0; // Register internal plugin for i in internal::get_internal_plugin() { let name = i.name.to_string(); match list { Some(list) if !list.contains(&name) => { log::debug!("Plugin {} not in list, skipping", name); continue; } _ => {} } log::debug!("Registering internal plugin {}", i.name); plugins.insert(i.name.to_string(), FinalizePlugin::new(i.entry)); count += 1; } // Register external plugin for (name, data) in external_plugins.into_iter() { match list { Some(list) if !list.contains(&name) => { log::debug!("Plugin {} not in list, skipping", name); continue; } _ => {} } log::debug!("Registering external plugin {}", name); let (_source, config) = data.into_iter().next().unwrap_or_else(|| { panic!( "Internal Error: Plugin {} has no source configured, this should have been caught by validation!", name ) }); // Assume every plugin prepared (or definitely unavailable) now if let Some(reason) = config.runtime.error { log::warn!("Plugin {} is not available: {}", name, reason); continue; } let metadata = read_manifest(config.runtime.fspath, &name)?; let plugin_type = get_plugin_type(metadata.plugin_type.clone(), &metadata.entrypoint); let entry = get_entry(plugin_type, &name)?; plugins.insert( name, FinalizePlugin::new(entry) .with_metadata(metadata) .with_argument(config.config), ); count += 1; } log::info!("Registered {} plugins", count); Ok(plugins) } /// Calls a registered plugin by name, passing the configuration, execution context and event sender. /// /// # Arguments /// /// * `plugins` - Map of registered plugin names to their [`FinalizePlugin`] instances /// * `name` - Name of the plugin to invoke /// * `config` - Configuration value passed to the plugin /// * `ctx` - Execution context for the plugin /// * `event_tx` - Event sender for plugin lifecycle events /// /// # Returns /// /// Returns `Ok(())` on successful plugin execution, or a [`PluginError`] if the plugin /// is not found or execution fails. pub async fn call_named_plugin( plugins: &PluginMap, name: &str, config: serde_yaml::Value, ctx: &ExecutionContext, event_tx: &EventSender, ) -> PluginResult { let plugin = plugins .get(name) .ok_or_else(|| PluginError::UnknownPlugin(name.to_string()))?; plugin.call(config, ctx, event_tx).await }