1use serde::{Deserialize, Serialize};
8use thulp_core::ToolDefinition;
9
10pub fn parse_query(query: &str) -> Result<QueryCriteria> {
12 let lower_query = query.to_lowercase();
16
17 if lower_query.contains("and") {
18 let parts: Vec<&str> = query.split(" and ").collect();
19 let criteria = parts
20 .into_iter()
21 .map(parse_single_criterion)
22 .collect::<std::result::Result<Vec<_>, _>>()?;
23 Ok(QueryCriteria::And(criteria))
24 } else if lower_query.contains("or") {
25 let parts: Vec<&str> = query.split(" or ").collect();
26 let criteria = parts
27 .into_iter()
28 .map(parse_single_criterion)
29 .collect::<std::result::Result<Vec<_>, _>>()?;
30 Ok(QueryCriteria::Or(criteria))
31 } else {
32 parse_single_criterion(query)
33 }
34}
35
36fn parse_single_criterion(criterion: &str) -> Result<QueryCriteria> {
37 let lower = criterion.to_lowercase();
38
39 if lower.starts_with("name:") {
40 let name = criterion[5..].trim();
41 Ok(QueryCriteria::Name(name.to_string()))
42 } else if lower.starts_with("has:") {
43 let param = criterion[4..].trim();
44 Ok(QueryCriteria::HasParameter(param.to_string()))
45 } else if lower.starts_with("min:") {
46 let count: usize = criterion[4..]
47 .trim()
48 .parse()
49 .map_err(|_| QueryError::Parse("Invalid number for min".to_string()))?;
50 Ok(QueryCriteria::MinParameters(count))
51 } else if lower.starts_with("max:") {
52 let count: usize = criterion[4..]
53 .trim()
54 .parse()
55 .map_err(|_| QueryError::Parse("Invalid number for max".to_string()))?;
56 Ok(QueryCriteria::MaxParameters(count))
57 } else if lower.starts_with("desc:") || lower.starts_with("description:") {
58 let desc = if lower.starts_with("desc:") {
59 criterion[5..].trim()
60 } else {
61 criterion[12..].trim()
62 };
63 Ok(QueryCriteria::Description(desc.to_string()))
64 } else {
65 Ok(QueryCriteria::Name(criterion.to_string()))
67 }
68}
69
70pub type Result<T> = std::result::Result<T, QueryError>;
72
73#[derive(Debug, thiserror::Error)]
75pub enum QueryError {
76 #[error("Parse error: {0}")]
77 Parse(String),
78
79 #[error("Invalid query: {0}")]
80 Invalid(String),
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub enum QueryCriteria {
86 Name(String),
88
89 Description(String),
91
92 HasParameter(String),
94
95 MinParameters(usize),
97
98 MaxParameters(usize),
100
101 And(Vec<QueryCriteria>),
103
104 Or(Vec<QueryCriteria>),
106
107 Not(Box<QueryCriteria>),
109}
110
111impl QueryCriteria {
112 pub fn matches(&self, tool: &ToolDefinition) -> bool {
114 match self {
115 QueryCriteria::Name(pattern) => {
116 if pattern.contains('*') {
117 let regex = pattern.replace('*', ".*");
118 regex::Regex::new(®ex)
119 .map(|re| re.is_match(&tool.name))
120 .unwrap_or(false)
121 } else {
122 tool.name.contains(pattern)
123 }
124 }
125 QueryCriteria::Description(keyword) => tool
126 .description
127 .to_lowercase()
128 .contains(&keyword.to_lowercase()),
129 QueryCriteria::HasParameter(param_name) => {
130 tool.parameters.iter().any(|p| p.name == *param_name)
131 }
132 QueryCriteria::MinParameters(min) => tool.parameters.len() >= *min,
133 QueryCriteria::MaxParameters(max) => tool.parameters.len() <= *max,
134 QueryCriteria::And(criteria) => criteria.iter().all(|c| c.matches(tool)),
135 QueryCriteria::Or(criteria) => criteria.iter().any(|c| c.matches(tool)),
136 QueryCriteria::Not(criteria) => !criteria.matches(tool),
137 }
138 }
139}
140
141#[derive(Debug, Default)]
143pub struct QueryBuilder {
144 criteria: Vec<QueryCriteria>,
145}
146
147impl QueryBuilder {
148 pub fn new() -> Self {
150 Self::default()
151 }
152
153 pub fn name(mut self, pattern: impl Into<String>) -> Self {
155 self.criteria.push(QueryCriteria::Name(pattern.into()));
156 self
157 }
158
159 pub fn description(mut self, keyword: impl Into<String>) -> Self {
161 self.criteria
162 .push(QueryCriteria::Description(keyword.into()));
163 self
164 }
165
166 pub fn has_parameter(mut self, param_name: impl Into<String>) -> Self {
168 self.criteria
169 .push(QueryCriteria::HasParameter(param_name.into()));
170 self
171 }
172
173 pub fn min_parameters(mut self, min: usize) -> Self {
175 self.criteria.push(QueryCriteria::MinParameters(min));
176 self
177 }
178
179 pub fn max_parameters(mut self, max: usize) -> Self {
181 self.criteria.push(QueryCriteria::MaxParameters(max));
182 self
183 }
184
185 pub fn build(self) -> Query {
187 Query {
188 criteria: if self.criteria.len() == 1 {
189 self.criteria.into_iter().next().unwrap()
190 } else {
191 QueryCriteria::And(self.criteria)
192 },
193 }
194 }
195}
196
197#[derive(Debug, Clone)]
199pub struct Query {
200 criteria: QueryCriteria,
201}
202
203impl Query {
204 pub fn new(criteria: QueryCriteria) -> Self {
206 Self { criteria }
207 }
208
209 pub fn execute(&self, tools: &[ToolDefinition]) -> Vec<ToolDefinition> {
211 tools
212 .iter()
213 .filter(|tool| self.criteria.matches(tool))
214 .cloned()
215 .collect()
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use thulp_core::Parameter;
223
224 fn create_test_tool(name: &str, description: &str, param_count: usize) -> ToolDefinition {
225 let mut builder = ToolDefinition::builder(name).description(description);
226
227 for i in 0..param_count {
228 builder = builder.parameter(Parameter::required_string(format!("param{}", i)));
229 }
230
231 builder.build()
232 }
233
234 #[test]
235 fn test_query_name() {
236 let tool = create_test_tool("file_read", "Read a file", 1);
237 let criteria = QueryCriteria::Name("file".to_string());
238 assert!(criteria.matches(&tool));
239 }
240
241 #[test]
242 fn test_query_name_wildcard() {
243 let tool = create_test_tool("file_read", "Read a file", 1);
244 let criteria = QueryCriteria::Name("file_*".to_string());
245 assert!(criteria.matches(&tool));
246 }
247
248 #[test]
249 fn test_query_description() {
250 let tool = create_test_tool("file_read", "Read a file from disk", 1);
251 let criteria = QueryCriteria::Description("disk".to_string());
252 assert!(criteria.matches(&tool));
253 }
254
255 #[test]
256 fn test_query_has_parameter() {
257 let tool = ToolDefinition::builder("test")
258 .parameter(Parameter::required_string("path"))
259 .build();
260
261 let criteria = QueryCriteria::HasParameter("path".to_string());
262 assert!(criteria.matches(&tool));
263 }
264
265 #[test]
266 fn test_query_min_parameters() {
267 let tool = create_test_tool("test", "Test", 3);
268 let criteria = QueryCriteria::MinParameters(2);
269 assert!(criteria.matches(&tool));
270 }
271
272 #[test]
273 fn test_query_max_parameters() {
274 let tool = create_test_tool("test", "Test", 2);
275 let criteria = QueryCriteria::MaxParameters(3);
276 assert!(criteria.matches(&tool));
277 }
278
279 #[test]
280 fn test_query_and() {
281 let tool = create_test_tool("file_read", "Read a file", 2);
282 let criteria = QueryCriteria::And(vec![
283 QueryCriteria::Name("file".to_string()),
284 QueryCriteria::MinParameters(2),
285 ]);
286 assert!(criteria.matches(&tool));
287 }
288
289 #[test]
290 fn test_query_or() {
291 let tool = create_test_tool("file_read", "Read a file", 1);
292 let criteria = QueryCriteria::Or(vec![
293 QueryCriteria::Name("network".to_string()),
294 QueryCriteria::Name("file".to_string()),
295 ]);
296 assert!(criteria.matches(&tool));
297 }
298
299 #[test]
300 fn test_query_not() {
301 let tool = create_test_tool("file_read", "Read a file", 1);
302 let criteria = QueryCriteria::Not(Box::new(QueryCriteria::Name("network".to_string())));
303 assert!(criteria.matches(&tool));
304 }
305
306 #[test]
307 fn test_query_builder() {
308 let query = QueryBuilder::new().name("file").min_parameters(1).build();
309
310 let tools = vec![
311 create_test_tool("file_read", "Read", 2),
312 create_test_tool("network_get", "Get", 1),
313 ];
314
315 let results = query.execute(&tools);
316 assert_eq!(results.len(), 1);
317 assert_eq!(results[0].name, "file_read");
318 }
319
320 #[test]
321 fn test_query_execute() {
322 let query = Query::new(QueryCriteria::MinParameters(2));
323
324 let tools = vec![
325 create_test_tool("tool1", "Test 1", 1),
326 create_test_tool("tool2", "Test 2", 2),
327 create_test_tool("tool3", "Test 3", 3),
328 ];
329
330 let results = query.execute(&tools);
331 assert_eq!(results.len(), 2);
332 }
333
334 #[test]
335 fn test_parse_query_name() {
336 let criteria = parse_query("search").unwrap();
337 assert!(matches!(criteria, QueryCriteria::Name(_)));
338 }
339
340 #[test]
341 fn test_parse_query_with_prefix() {
342 let criteria = parse_query("name:search").unwrap();
343 assert!(matches!(criteria, QueryCriteria::Name(_)));
344 }
345
346 #[test]
347 fn test_parse_query_has_parameter() {
348 let criteria = parse_query("has:path").unwrap();
349 assert!(matches!(criteria, QueryCriteria::HasParameter(_)));
350 }
351
352 #[test]
353 fn test_parse_query_min_parameters() {
354 let criteria = parse_query("min:2").unwrap();
355 assert!(matches!(criteria, QueryCriteria::MinParameters(2)));
356 }
357
358 #[test]
359 fn test_parse_query_max_parameters() {
360 let criteria = parse_query("max:5").unwrap();
361 assert!(matches!(criteria, QueryCriteria::MaxParameters(5)));
362 }
363
364 #[test]
365 fn test_parse_query_description() {
366 let criteria = parse_query("desc:file").unwrap();
367 assert!(matches!(criteria, QueryCriteria::Description(_)));
368 }
369
370 #[test]
371 fn test_parse_query_and() {
372 let criteria = parse_query("name:search and has:query").unwrap();
373 assert!(matches!(criteria, QueryCriteria::And(_)));
374 }
375
376 #[test]
377 fn test_parse_query_or() {
378 let criteria = parse_query("name:search or name:find").unwrap();
379 assert!(matches!(criteria, QueryCriteria::Or(_)));
380 }
381}