ironflow_core/providers/http/tools/
routing.rs1use std::collections::HashSet;
8use std::fmt;
9
10pub const CONNECTOR_SEPARATOR: &str = "__";
12
13#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum RoutingError {
28 AmbiguousToolName {
30 name: String,
32 },
33 ConnectorNotFound {
35 connector: String,
37 tool: String,
39 },
40}
41
42impl fmt::Display for RoutingError {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 match self {
45 Self::AmbiguousToolName { name } => {
46 write!(f, "tool '{name}' has no connector prefix")
47 }
48 Self::ConnectorNotFound { connector, tool } => {
49 write!(f, "connector '{connector}' not found for tool '{tool}'")
50 }
51 }
52 }
53}
54
55impl std::error::Error for RoutingError {}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct RoutedTool {
60 pub connector: String,
62 pub tool: String,
64 pub registry_key: String,
66}
67
68pub fn route_tool_call(
96 name: &str,
97 known_connectors: &HashSet<String>,
98) -> Result<RoutedTool, RoutingError> {
99 let Some((connector, tool)) = name.split_once(CONNECTOR_SEPARATOR) else {
100 return Err(RoutingError::AmbiguousToolName {
101 name: name.to_string(),
102 });
103 };
104
105 if !known_connectors.contains(connector) {
106 return Err(RoutingError::ConnectorNotFound {
107 connector: connector.to_string(),
108 tool: tool.to_string(),
109 });
110 }
111
112 Ok(RoutedTool {
113 connector: connector.to_string(),
114 tool: tool.to_string(),
115 registry_key: name.to_string(),
116 })
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 fn connectors(names: &[&str]) -> HashSet<String> {
124 names.iter().map(|s| s.to_string()).collect()
125 }
126
127 #[test]
128 fn route_prefixed_tool_resolves() {
129 let known = connectors(&["grafana", "slack"]);
130 let routed = route_tool_call("grafana__query", &known).unwrap();
131 assert_eq!(routed.connector, "grafana");
132 assert_eq!(routed.tool, "query");
133 assert_eq!(routed.registry_key, "grafana__query");
134 }
135
136 #[test]
137 fn route_bare_name_rejected() {
138 let known = connectors(&["grafana"]);
139 let err = route_tool_call("query", &known).unwrap_err();
140 assert_eq!(
141 err,
142 RoutingError::AmbiguousToolName {
143 name: "query".to_string()
144 }
145 );
146 assert_eq!(err.to_string(), "tool 'query' has no connector prefix");
147 }
148
149 #[test]
150 fn route_unknown_connector_rejected() {
151 let known = connectors(&["grafana"]);
152 let err = route_tool_call("unknown__search", &known).unwrap_err();
153 assert_eq!(
154 err,
155 RoutingError::ConnectorNotFound {
156 connector: "unknown".to_string(),
157 tool: "search".to_string(),
158 }
159 );
160 }
161
162 #[test]
163 fn route_nested_underscores_preserved() {
164 let known = connectors(&["prefix"]);
165 let routed = route_tool_call("prefix__deep__search", &known).unwrap();
166 assert_eq!(routed.connector, "prefix");
167 assert_eq!(routed.tool, "deep__search");
168 }
169
170 #[test]
171 fn route_empty_prefix_rejected() {
172 let known = connectors(&["grafana"]);
173 let err = route_tool_call("__search", &known).unwrap_err();
174 assert_eq!(
175 err,
176 RoutingError::ConnectorNotFound {
177 connector: "".to_string(),
178 tool: "search".to_string(),
179 }
180 );
181 }
182
183 #[test]
184 fn route_empty_tool_name_resolves_if_connector_known() {
185 let known = connectors(&["prefix"]);
186 let routed = route_tool_call("prefix__", &known).unwrap();
187 assert_eq!(routed.connector, "prefix");
188 assert_eq!(routed.tool, "");
189 }
190
191 #[test]
192 fn display_ambiguous_tool_name() {
193 let err = RoutingError::AmbiguousToolName {
194 name: "search".to_string(),
195 };
196 assert_eq!(err.to_string(), "tool 'search' has no connector prefix");
197 }
198
199 #[test]
200 fn display_connector_not_found() {
201 let err = RoutingError::ConnectorNotFound {
202 connector: "unknown".to_string(),
203 tool: "search".to_string(),
204 };
205 assert_eq!(
206 err.to_string(),
207 "connector 'unknown' not found for tool 'search'"
208 );
209 }
210}