use crate::error::Error;
use crate::spec::{Filters, Spec};
use crate::tool::ToolMetadata;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct ToolRegistry {
tools: HashMap<String, ToolMetadata>,
operations: HashMap<String, (oas3::spec::Operation, String, String)>,
spec: Option<Spec>,
}
impl ToolRegistry {
#[must_use]
pub fn new() -> Self {
Self {
tools: HashMap::new(),
operations: HashMap::new(),
spec: None,
}
}
pub fn register_from_spec(
&mut self,
spec: Spec,
filters: Option<&Filters>,
skip_tool_descriptions: bool,
skip_parameter_descriptions: bool,
) -> Result<usize, Error> {
self.clear();
let tools_metadata =
spec.to_tool_metadata(filters, skip_tool_descriptions, skip_parameter_descriptions)?;
let mut registered_count = 0;
for tool in tools_metadata {
if let Some((operation, method, path)) = spec.get_operation(&tool.name) {
self.register_tool(tool, (operation.clone(), method, path))?;
registered_count += 1;
}
}
self.spec = Some(spec);
Ok(registered_count)
}
pub fn register_tool(
&mut self,
tool: ToolMetadata,
operation: (oas3::spec::Operation, String, String),
) -> Result<(), Error> {
let tool_name = tool.name.clone();
self.validate_tool(&tool)?;
self.tools.insert(tool_name.clone(), tool);
self.operations.insert(tool_name, operation);
Ok(())
}
fn validate_tool(&self, tool: &ToolMetadata) -> Result<(), Error> {
if tool.name.is_empty() {
return Err(Error::ToolGeneration(
"Tool name cannot be empty".to_string(),
));
}
if tool.method.is_empty() {
return Err(Error::ToolGeneration(
"Tool method cannot be empty".to_string(),
));
}
if tool.path.is_empty() {
return Err(Error::ToolGeneration(
"Tool path cannot be empty".to_string(),
));
}
if self.tools.contains_key(&tool.name) {
return Err(Error::ToolGeneration(format!(
"Tool '{}' already exists",
tool.name
)));
}
Ok(())
}
#[must_use]
pub fn get_tool(&self, name: &str) -> Option<&ToolMetadata> {
self.tools.get(name)
}
#[must_use]
pub fn get_operation(
&self,
tool_name: &str,
) -> Option<&(oas3::spec::Operation, String, String)> {
self.operations.get(tool_name)
}
#[must_use]
pub fn get_tool_names(&self) -> Vec<String> {
self.tools.keys().cloned().collect()
}
#[must_use]
pub fn get_all_tools(&self) -> Vec<&ToolMetadata> {
self.tools.values().collect()
}
#[must_use]
pub fn tool_count(&self) -> usize {
self.tools.len()
}
#[must_use]
pub fn has_tool(&self, name: &str) -> bool {
self.tools.contains_key(name)
}
pub fn remove_tool(&mut self, name: &str) -> Option<ToolMetadata> {
self.operations.remove(name);
self.tools.remove(name)
}
pub fn clear(&mut self) {
self.tools.clear();
self.operations.clear();
self.spec = None;
}
#[must_use]
pub fn get_spec(&self) -> Option<&Spec> {
self.spec.as_ref()
}
#[must_use]
pub fn get_stats(&self) -> ToolRegistryStats {
let mut method_counts = HashMap::new();
let mut path_counts = HashMap::new();
for tool in self.tools.values() {
*method_counts.entry(tool.method.clone()).or_insert(0) += 1;
*path_counts.entry(tool.path.clone()).or_insert(0) += 1;
}
ToolRegistryStats {
total_tools: self.tools.len(),
method_distribution: method_counts,
unique_paths: path_counts.len(),
has_spec: self.spec.is_some(),
}
}
pub fn validate_registry(&self) -> Result<(), Error> {
for tool in self.tools.values() {
if !self.operations.contains_key(&tool.name) {
return Err(Error::ToolGeneration(format!(
"Missing operation for tool '{}'",
tool.name
)));
}
Self::validate_tool_metadata(&tool.name, tool)?;
}
for operation_name in self.operations.keys() {
if !self.tools.contains_key(operation_name) {
return Err(Error::ToolGeneration(format!(
"Orphaned operation '{operation_name}'"
)));
}
}
Ok(())
}
fn validate_tool_metadata(tool_name: &str, tool_metadata: &ToolMetadata) -> Result<(), Error> {
if !tool_metadata.parameters.is_object() {
return Err(Error::Validation(format!(
"Tool '{tool_name}' has invalid parameters schema - must be an object"
)));
}
let schema_obj = tool_metadata.parameters.as_object().unwrap();
if let Some(properties) = schema_obj.get("properties") {
if !properties.is_object() {
return Err(Error::Validation(format!(
"Tool '{tool_name}' properties field must be an object"
)));
}
} else {
return Err(Error::Validation(format!(
"Tool '{tool_name}' is missing properties field in parameters schema"
)));
}
if let Some(required) = schema_obj.get("required")
&& !required.is_array()
{
return Err(Error::Validation(format!(
"Tool '{tool_name}' required field must be an array"
)));
}
let valid_methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
if !valid_methods.contains(&tool_metadata.method.to_uppercase().as_str()) {
return Err(Error::Validation(format!(
"Tool '{}' has invalid HTTP method: {}",
tool_name, tool_metadata.method
)));
}
if tool_metadata.path.is_empty() {
return Err(Error::Validation(format!(
"Tool '{tool_name}' has empty path"
)));
}
Ok(())
}
}
impl Default for ToolRegistry {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct ToolRegistryStats {
pub total_tools: usize,
pub method_distribution: HashMap<String, usize>,
pub unique_paths: usize,
pub has_spec: bool,
}
impl ToolRegistryStats {
#[must_use]
pub fn summary(&self) -> String {
let methods: Vec<String> = self
.method_distribution
.iter()
.map(|(method, count)| format!("{}: {}", method.to_uppercase(), count))
.collect();
format!(
"Tools: {}, Methods: [{}], Paths: {}, Spec: {}",
self.total_tools,
methods.join(", "),
self.unique_paths,
if self.has_spec { "loaded" } else { "none" }
)
}
}