ryo-plugin-runtime 0.2.0

[experimental] WASM plugin runtime for ryo mutations (registry + executor)
Documentation
//! Mutation Registry - Unified access to builtin and plugin mutations

use ryo_plugin_loader::{LoadedPlugin, LoaderError, PluginLoader, TransformDef};
use std::collections::HashMap;
use std::path::Path;

/// Error type for registry operations
#[derive(Debug, thiserror::Error)]
pub enum RegistryError {
    #[error("Failed to create plugin loader: {0}")]
    LoaderCreation(#[source] LoaderError),

    #[error("Failed to load plugin: {0}")]
    PluginLoad(#[source] LoaderError),

    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    #[error("Plugin not found: {0}")]
    PluginNotFound(String),
}

/// Registry of all available mutations (builtin + plugins)
///
/// Provides unified access to both built-in Rust mutations and
/// dynamically loaded WASM plugins.
pub struct MutationRegistry {
    /// Plugin loader instance
    loader: PluginLoader,
    /// Loaded WASM plugins by name
    plugins: HashMap<String, LoadedPlugin>,
}

impl MutationRegistry {
    /// Create a new registry with plugin support
    pub fn new() -> Result<Self, RegistryError> {
        let loader = PluginLoader::new().map_err(RegistryError::LoaderCreation)?;

        Ok(Self {
            loader,
            plugins: HashMap::new(),
        })
    }

    /// Load a single plugin from bytes
    pub fn load_plugin(&mut self, wasm_bytes: &[u8]) -> Result<String, RegistryError> {
        let plugin = self
            .loader
            .load(wasm_bytes)
            .map_err(RegistryError::PluginLoad)?;
        let name = plugin.manifest.name.clone();

        tracing::info!(
            "Registered plugin mutation: {} (tier {})",
            name,
            plugin.manifest.tier
        );

        self.plugins.insert(name.clone(), plugin);
        Ok(name)
    }

    /// Load all plugins from a directory
    pub fn load_plugins_from_dir(&mut self, dir: &Path) -> Result<Vec<String>, RegistryError> {
        let mut loaded = Vec::new();

        if !dir.exists() {
            tracing::debug!("Plugin directory does not exist: {:?}", dir);
            return Ok(loaded);
        }

        for entry in std::fs::read_dir(dir)? {
            let entry = entry?;
            let path = entry.path();

            // Only load .wasm files
            if path.extension().is_some_and(|ext| ext == "wasm") {
                match std::fs::read(&path) {
                    Ok(bytes) => match self.load_plugin(&bytes) {
                        Ok(name) => {
                            tracing::info!("Loaded plugin from {:?}: {}", path, name);
                            loaded.push(name);
                        }
                        Err(e) => {
                            tracing::warn!("Failed to load plugin {:?}: {}", path, e);
                        }
                    },
                    Err(e) => {
                        tracing::warn!("Failed to read plugin file {:?}: {}", path, e);
                    }
                }
            }
        }

        Ok(loaded)
    }

    /// Get a loaded plugin by name
    pub fn get_plugin(&self, name: &str) -> Option<&LoadedPlugin> {
        self.plugins.get(name)
    }

    /// Get a mutable reference to a loaded plugin
    pub fn get_plugin_mut(&mut self, name: &str) -> Option<&mut LoadedPlugin> {
        self.plugins.get_mut(name)
    }

    /// Check if a plugin is loaded
    pub fn has_plugin(&self, name: &str) -> bool {
        self.plugins.contains_key(name)
    }

    /// Get all loaded plugin names
    pub fn plugin_names(&self) -> Vec<&str> {
        self.plugins.keys().map(|s| s.as_str()).collect()
    }

    /// Get plugin info for display
    pub fn plugin_info(&self) -> Vec<PluginInfo> {
        self.plugins
            .values()
            .map(|p| PluginInfo {
                name: p.manifest.name.clone(),
                description: p.manifest.description.clone(),
                tier: p.manifest.tier,
                is_template: matches!(p.manifest.transform, TransformDef::Template(_)),
            })
            .collect()
    }
}

/// Information about a loaded plugin
#[derive(Debug, Clone)]
pub struct PluginInfo {
    pub name: String,
    pub description: String,
    pub tier: u8,
    pub is_template: bool,
}

impl std::fmt::Display for PluginInfo {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let transform_type = if self.is_template { "template" } else { "wasm" };
        write!(
            f,
            "{} (tier {}, {}): {}",
            self.name, self.tier, transform_type, self.description
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_registry_creation() {
        let registry = MutationRegistry::new();
        assert!(registry.is_ok());
    }
}