use std::collections::HashSet;
use std::fmt;
pub const CONNECTOR_SEPARATOR: &str = "__";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RoutingError {
AmbiguousToolName {
name: String,
},
ConnectorNotFound {
connector: String,
tool: String,
},
}
impl fmt::Display for RoutingError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::AmbiguousToolName { name } => {
write!(f, "tool '{name}' has no connector prefix")
}
Self::ConnectorNotFound { connector, tool } => {
write!(f, "connector '{connector}' not found for tool '{tool}'")
}
}
}
}
impl std::error::Error for RoutingError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RoutedTool {
pub connector: String,
pub tool: String,
pub registry_key: String,
}
pub fn route_tool_call(
name: &str,
known_connectors: &HashSet<String>,
) -> Result<RoutedTool, RoutingError> {
let Some((connector, tool)) = name.split_once(CONNECTOR_SEPARATOR) else {
return Err(RoutingError::AmbiguousToolName {
name: name.to_string(),
});
};
if !known_connectors.contains(connector) {
return Err(RoutingError::ConnectorNotFound {
connector: connector.to_string(),
tool: tool.to_string(),
});
}
Ok(RoutedTool {
connector: connector.to_string(),
tool: tool.to_string(),
registry_key: name.to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn connectors(names: &[&str]) -> HashSet<String> {
names.iter().map(|s| s.to_string()).collect()
}
#[test]
fn route_prefixed_tool_resolves() {
let known = connectors(&["grafana", "slack"]);
let routed = route_tool_call("grafana__query", &known).unwrap();
assert_eq!(routed.connector, "grafana");
assert_eq!(routed.tool, "query");
assert_eq!(routed.registry_key, "grafana__query");
}
#[test]
fn route_bare_name_rejected() {
let known = connectors(&["grafana"]);
let err = route_tool_call("query", &known).unwrap_err();
assert_eq!(
err,
RoutingError::AmbiguousToolName {
name: "query".to_string()
}
);
assert_eq!(err.to_string(), "tool 'query' has no connector prefix");
}
#[test]
fn route_unknown_connector_rejected() {
let known = connectors(&["grafana"]);
let err = route_tool_call("unknown__search", &known).unwrap_err();
assert_eq!(
err,
RoutingError::ConnectorNotFound {
connector: "unknown".to_string(),
tool: "search".to_string(),
}
);
}
#[test]
fn route_nested_underscores_preserved() {
let known = connectors(&["prefix"]);
let routed = route_tool_call("prefix__deep__search", &known).unwrap();
assert_eq!(routed.connector, "prefix");
assert_eq!(routed.tool, "deep__search");
}
#[test]
fn route_empty_prefix_rejected() {
let known = connectors(&["grafana"]);
let err = route_tool_call("__search", &known).unwrap_err();
assert_eq!(
err,
RoutingError::ConnectorNotFound {
connector: "".to_string(),
tool: "search".to_string(),
}
);
}
#[test]
fn route_empty_tool_name_resolves_if_connector_known() {
let known = connectors(&["prefix"]);
let routed = route_tool_call("prefix__", &known).unwrap();
assert_eq!(routed.connector, "prefix");
assert_eq!(routed.tool, "");
}
#[test]
fn display_ambiguous_tool_name() {
let err = RoutingError::AmbiguousToolName {
name: "search".to_string(),
};
assert_eq!(err.to_string(), "tool 'search' has no connector prefix");
}
#[test]
fn display_connector_not_found() {
let err = RoutingError::ConnectorNotFound {
connector: "unknown".to_string(),
tool: "search".to_string(),
};
assert_eq!(
err.to_string(),
"connector 'unknown' not found for tool 'search'"
);
}
}