use std::collections::HashMap;
use tracing::info;
use crate::error::{Result, ZeptoError};
use super::types::{Plugin, PluginToolDef};
pub struct PluginRegistry {
plugins: HashMap<String, Plugin>,
tool_to_plugin: HashMap<String, String>,
}
impl PluginRegistry {
pub fn new() -> Self {
Self {
plugins: HashMap::new(),
tool_to_plugin: HashMap::new(),
}
}
pub fn register(&mut self, plugin: Plugin) -> Result<()> {
let plugin_name = plugin.name().to_string();
for tool in &plugin.manifest.tools {
if let Some(existing_plugin) = self.tool_to_plugin.get(&tool.name) {
if existing_plugin != &plugin_name {
return Err(ZeptoError::Config(format!(
"Tool name '{}' from plugin '{}' conflicts with existing tool from plugin '{}'",
tool.name, plugin_name, existing_plugin
)));
}
}
}
if self.plugins.contains_key(&plugin_name) {
self.tool_to_plugin.retain(|_, pname| pname != &plugin_name);
}
for tool in &plugin.manifest.tools {
self.tool_to_plugin
.insert(tool.name.clone(), plugin_name.clone());
}
info!(
plugin = %plugin_name,
tools = plugin.tool_count(),
"Registered plugin"
);
self.plugins.insert(plugin_name, plugin);
Ok(())
}
pub fn get_plugin(&self, name: &str) -> Option<&Plugin> {
self.plugins.get(name)
}
pub fn get_tool_plugin(&self, tool_name: &str) -> Option<(&Plugin, &PluginToolDef)> {
let plugin_name = self.tool_to_plugin.get(tool_name)?;
let plugin = self.plugins.get(plugin_name)?;
let tool_def = plugin.manifest.tools.iter().find(|t| t.name == tool_name)?;
Some((plugin, tool_def))
}
pub fn plugin_count(&self) -> usize {
self.plugins.len()
}
pub fn tool_count(&self) -> usize {
self.tool_to_plugin.len()
}
pub fn list_plugins(&self) -> Vec<&Plugin> {
self.plugins.values().collect()
}
pub fn list_tools(&self) -> Vec<(&str, &str)> {
self.tool_to_plugin
.iter()
.map(|(tool, plugin)| (tool.as_str(), plugin.as_str()))
.collect()
}
pub fn is_tool_from_plugin(&self, tool_name: &str) -> bool {
self.tool_to_plugin.contains_key(tool_name)
}
pub fn all_tool_defs(&self) -> Vec<&PluginToolDef> {
self.plugins
.values()
.flat_map(|p| p.manifest.tools.iter())
.collect()
}
pub fn plugin_for_tool(&self, tool_name: &str) -> Option<&str> {
self.tool_to_plugin.get(tool_name).map(|s| s.as_str())
}
}
impl Default for PluginRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plugins::types::{PluginManifest, PluginToolDef};
use serde_json::json;
use std::path::PathBuf;
fn make_plugin(name: &str, tool_names: &[&str]) -> Plugin {
let tools: Vec<PluginToolDef> = tool_names
.iter()
.map(|tn| PluginToolDef {
name: tn.to_string(),
description: format!("Tool {}", tn),
parameters: json!({
"type": "object",
"properties": {},
"required": []
}),
command: format!("echo {}", tn),
working_dir: None,
timeout_secs: None,
env: None,
category: None,
})
.collect();
let manifest = PluginManifest {
name: name.to_string(),
version: "1.0.0".to_string(),
description: format!("Plugin {}", name),
author: None,
tools,
execution: "command".to_string(),
binary: None,
};
Plugin::new(manifest, PathBuf::from(format!("/tmp/{}", name)))
}
#[test]
fn test_registry_new_is_empty() {
let registry = PluginRegistry::new();
assert_eq!(registry.plugin_count(), 0);
assert_eq!(registry.tool_count(), 0);
assert!(registry.list_plugins().is_empty());
assert!(registry.list_tools().is_empty());
}
#[test]
fn test_registry_default_is_empty() {
let registry = PluginRegistry::default();
assert_eq!(registry.plugin_count(), 0);
assert_eq!(registry.tool_count(), 0);
}
#[test]
fn test_register_and_lookup_plugin() {
let mut registry = PluginRegistry::new();
let plugin = make_plugin("git-tools", &["git_status", "git_log"]);
registry.register(plugin).unwrap();
let found = registry.get_plugin("git-tools");
assert!(found.is_some());
assert_eq!(found.unwrap().name(), "git-tools");
assert_eq!(found.unwrap().tool_count(), 2);
}
#[test]
fn test_register_plugin_not_found() {
let registry = PluginRegistry::new();
assert!(registry.get_plugin("nonexistent").is_none());
}
#[test]
fn test_tool_name_conflict_detection() {
let mut registry = PluginRegistry::new();
let plugin1 = make_plugin("plugin-a", &["shared_tool"]);
let plugin2 = make_plugin("plugin-b", &["shared_tool"]);
registry.register(plugin1).unwrap();
let result = registry.register(plugin2);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("conflicts"));
assert!(err_msg.contains("shared_tool"));
assert!(err_msg.contains("plugin-a"));
assert!(err_msg.contains("plugin-b"));
}
#[test]
fn test_get_tool_by_name_returns_correct_plugin() {
let mut registry = PluginRegistry::new();
let plugin1 = make_plugin("plugin-a", &["tool_alpha"]);
let plugin2 = make_plugin("plugin-b", &["tool_beta"]);
registry.register(plugin1).unwrap();
registry.register(plugin2).unwrap();
let (plugin, tool_def) = registry.get_tool_plugin("tool_alpha").unwrap();
assert_eq!(plugin.name(), "plugin-a");
assert_eq!(tool_def.name, "tool_alpha");
let (plugin, tool_def) = registry.get_tool_plugin("tool_beta").unwrap();
assert_eq!(plugin.name(), "plugin-b");
assert_eq!(tool_def.name, "tool_beta");
}
#[test]
fn test_get_tool_plugin_not_found() {
let registry = PluginRegistry::new();
assert!(registry.get_tool_plugin("nonexistent_tool").is_none());
}
#[test]
fn test_list_plugins() {
let mut registry = PluginRegistry::new();
registry
.register(make_plugin("alpha", &["tool_a"]))
.unwrap();
registry.register(make_plugin("beta", &["tool_b"])).unwrap();
registry
.register(make_plugin("gamma", &["tool_c"]))
.unwrap();
let plugins = registry.list_plugins();
assert_eq!(plugins.len(), 3);
let names: Vec<&str> = plugins.iter().map(|p| p.name()).collect();
assert!(names.contains(&"alpha"));
assert!(names.contains(&"beta"));
assert!(names.contains(&"gamma"));
}
#[test]
fn test_list_tools() {
let mut registry = PluginRegistry::new();
registry
.register(make_plugin("my-plugin", &["tool_x", "tool_y"]))
.unwrap();
let tools = registry.list_tools();
assert_eq!(tools.len(), 2);
let tool_names: Vec<&str> = tools.iter().map(|(name, _)| *name).collect();
assert!(tool_names.contains(&"tool_x"));
assert!(tool_names.contains(&"tool_y"));
for (_, plugin_name) in &tools {
assert_eq!(*plugin_name, "my-plugin");
}
}
#[test]
fn test_plugin_count_and_tool_count() {
let mut registry = PluginRegistry::new();
registry.register(make_plugin("p1", &["t1", "t2"])).unwrap();
registry.register(make_plugin("p2", &["t3"])).unwrap();
assert_eq!(registry.plugin_count(), 2);
assert_eq!(registry.tool_count(), 3);
}
#[test]
fn test_is_tool_from_plugin() {
let mut registry = PluginRegistry::new();
registry
.register(make_plugin("my-plugin", &["plugin_tool"]))
.unwrap();
assert!(registry.is_tool_from_plugin("plugin_tool"));
assert!(!registry.is_tool_from_plugin("builtin_tool"));
assert!(!registry.is_tool_from_plugin("nonexistent"));
}
#[test]
fn test_re_register_same_plugin_replaces() {
let mut registry = PluginRegistry::new();
let plugin_v1 = make_plugin("evolving", &["old_tool"]);
registry.register(plugin_v1).unwrap();
assert!(registry.is_tool_from_plugin("old_tool"));
assert_eq!(registry.tool_count(), 1);
let plugin_v2 = make_plugin("evolving", &["new_tool"]);
registry.register(plugin_v2).unwrap();
assert!(!registry.is_tool_from_plugin("old_tool"));
assert!(registry.is_tool_from_plugin("new_tool"));
assert_eq!(registry.plugin_count(), 1);
assert_eq!(registry.tool_count(), 1);
}
#[test]
fn test_multiple_plugins_no_conflict() {
let mut registry = PluginRegistry::new();
registry
.register(make_plugin("git-tools", &["git_status", "git_log"]))
.unwrap();
registry
.register(make_plugin("docker-tools", &["docker_ps", "docker_build"]))
.unwrap();
registry
.register(make_plugin("k8s-tools", &["kubectl_get"]))
.unwrap();
assert_eq!(registry.plugin_count(), 3);
assert_eq!(registry.tool_count(), 5);
assert_eq!(
registry.get_tool_plugin("git_status").unwrap().0.name(),
"git-tools"
);
assert_eq!(
registry.get_tool_plugin("docker_ps").unwrap().0.name(),
"docker-tools"
);
assert_eq!(
registry.get_tool_plugin("kubectl_get").unwrap().0.name(),
"k8s-tools"
);
}
#[test]
fn test_get_tool_plugin_returns_correct_tool_def() {
let mut registry = PluginRegistry::new();
let mut plugin = make_plugin("multi-tool", &["tool_a", "tool_b"]);
plugin.manifest.tools[0].command = "command_a".to_string();
plugin.manifest.tools[1].command = "command_b".to_string();
registry.register(plugin).unwrap();
let (_, tool_a) = registry.get_tool_plugin("tool_a").unwrap();
assert_eq!(tool_a.command, "command_a");
let (_, tool_b) = registry.get_tool_plugin("tool_b").unwrap();
assert_eq!(tool_b.command, "command_b");
}
}