1use crate::error::Error;
2use crate::normalize_tag;
3use crate::tool::ToolMetadata;
4use crate::tool_generator::ToolGenerator;
5use oas3::Spec as Oas3Spec;
6use reqwest::Method;
7use serde_json::Value;
8
9#[derive(Debug, Clone)]
12pub struct Spec {
13 pub spec: Oas3Spec,
14}
15
16impl Spec {
17 pub fn from_value(json_value: Value) -> Result<Self, Error> {
19 let spec: Oas3Spec = serde_json::from_value(json_value)?;
20 Ok(Spec { spec })
21 }
22
23 pub fn to_tool_metadata(
25 &self,
26 tag_filter: Option<&[String]>,
27 method_filter: Option<&[reqwest::Method]>,
28 ) -> Result<Vec<ToolMetadata>, Error> {
29 let mut tools = Vec::new();
30
31 if let Some(paths) = &self.spec.paths {
32 for (path, path_item) in paths {
33 let operations = [
35 (Method::GET, &path_item.get),
36 (Method::POST, &path_item.post),
37 (Method::PUT, &path_item.put),
38 (Method::DELETE, &path_item.delete),
39 (Method::PATCH, &path_item.patch),
40 (Method::HEAD, &path_item.head),
41 (Method::OPTIONS, &path_item.options),
42 (Method::TRACE, &path_item.trace),
43 ];
44
45 for (method, operation_ref) in operations {
46 if let Some(operation) = operation_ref {
47 if let Some(filter_methods) = method_filter
49 && !filter_methods.contains(&method)
50 {
51 continue; }
53
54 if let Some(filter_tags) = tag_filter {
56 if !operation.tags.is_empty() {
57 let normalized_filter_tags: Vec<String> =
59 filter_tags.iter().map(|tag| normalize_tag(tag)).collect();
60
61 let has_matching_tag = operation.tags.iter().any(|operation_tag| {
62 let normalized_operation_tag = normalize_tag(operation_tag);
63 normalized_filter_tags.contains(&normalized_operation_tag)
64 });
65
66 if !has_matching_tag {
67 continue; }
69 } else {
70 continue; }
72 }
73
74 let tool_metadata = ToolGenerator::generate_tool_metadata(
75 operation,
76 method.to_string(),
77 path.clone(),
78 &self.spec,
79 )?;
80 tools.push(tool_metadata);
81 }
82 }
83 }
84 }
85
86 Ok(tools)
87 }
88
89 pub fn to_openapi_tools(
95 &self,
96 tag_filter: Option<&[String]>,
97 method_filter: Option<&[reqwest::Method]>,
98 base_url: Option<url::Url>,
99 default_headers: Option<reqwest::header::HeaderMap>,
100 ) -> Result<Vec<crate::tool::Tool>, Error> {
101 let tools_metadata = self.to_tool_metadata(tag_filter, method_filter)?;
103
104 crate::tool_generator::ToolGenerator::generate_openapi_tools(
106 tools_metadata,
107 base_url,
108 default_headers,
109 )
110 }
111
112 pub fn get_operation(
114 &self,
115 operation_id: &str,
116 ) -> Option<(&oas3::spec::Operation, String, String)> {
117 if let Some(paths) = &self.spec.paths {
118 for (path, path_item) in paths {
119 let operations = [
120 (Method::GET, &path_item.get),
121 (Method::POST, &path_item.post),
122 (Method::PUT, &path_item.put),
123 (Method::DELETE, &path_item.delete),
124 (Method::PATCH, &path_item.patch),
125 (Method::HEAD, &path_item.head),
126 (Method::OPTIONS, &path_item.options),
127 (Method::TRACE, &path_item.trace),
128 ];
129
130 for (method, operation_ref) in operations {
131 if let Some(operation) = operation_ref {
132 let default_id = format!(
133 "{}_{}",
134 method,
135 path.replace('/', "_").replace(['{', '}'], "")
136 );
137 let op_id = operation.operation_id.as_deref().unwrap_or(&default_id);
138
139 if op_id == operation_id {
140 return Some((operation, method.to_string(), path.clone()));
141 }
142 }
143 }
144 }
145 }
146 None
147 }
148
149 pub fn get_operation_ids(&self) -> Vec<String> {
151 let mut operation_ids = Vec::new();
152
153 if let Some(paths) = &self.spec.paths {
154 for (path, path_item) in paths {
155 let operations = [
156 (Method::GET, &path_item.get),
157 (Method::POST, &path_item.post),
158 (Method::PUT, &path_item.put),
159 (Method::DELETE, &path_item.delete),
160 (Method::PATCH, &path_item.patch),
161 (Method::HEAD, &path_item.head),
162 (Method::OPTIONS, &path_item.options),
163 (Method::TRACE, &path_item.trace),
164 ];
165
166 for (method, operation_ref) in operations {
167 if let Some(operation) = operation_ref {
168 let default_id = format!(
169 "{}_{}",
170 method,
171 path.replace('/', "_").replace(['{', '}'], "")
172 );
173 let op_id = operation.operation_id.as_deref().unwrap_or(&default_id);
174 operation_ids.push(op_id.to_string());
175 }
176 }
177 }
178 }
179
180 operation_ids
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use serde_json::json;
188
189 fn create_test_spec_with_tags() -> Spec {
190 let spec_json = json!({
191 "openapi": "3.0.3",
192 "info": {
193 "title": "Test API",
194 "version": "1.0.0"
195 },
196 "paths": {
197 "/pets": {
198 "get": {
199 "operationId": "listPets",
200 "tags": ["pet", "list"],
201 "responses": {
202 "200": {
203 "description": "List of pets"
204 }
205 }
206 },
207 "post": {
208 "operationId": "createPet",
209 "tags": ["pet"],
210 "responses": {
211 "201": {
212 "description": "Pet created"
213 }
214 }
215 }
216 },
217 "/users": {
218 "get": {
219 "operationId": "listUsers",
220 "tags": ["user"],
221 "responses": {
222 "200": {
223 "description": "List of users"
224 }
225 }
226 }
227 },
228 "/admin": {
229 "get": {
230 "operationId": "adminPanel",
231 "tags": ["admin", "management"],
232 "responses": {
233 "200": {
234 "description": "Admin panel"
235 }
236 }
237 }
238 },
239 "/public": {
240 "get": {
241 "operationId": "publicEndpoint",
242 "responses": {
243 "200": {
244 "description": "Public endpoint with no tags"
245 }
246 }
247 }
248 }
249 }
250 });
251
252 Spec::from_value(spec_json).expect("Failed to create test spec")
253 }
254
255 fn create_test_spec_with_mixed_case_tags() -> Spec {
256 let spec_json = json!({
257 "openapi": "3.0.3",
258 "info": {
259 "title": "Test API with Mixed Case Tags",
260 "version": "1.0.0"
261 },
262 "paths": {
263 "/camel": {
264 "get": {
265 "operationId": "camelCaseOperation",
266 "tags": ["userManagement"],
267 "responses": {
268 "200": {
269 "description": "camelCase tag"
270 }
271 }
272 }
273 },
274 "/pascal": {
275 "get": {
276 "operationId": "pascalCaseOperation",
277 "tags": ["UserManagement"],
278 "responses": {
279 "200": {
280 "description": "PascalCase tag"
281 }
282 }
283 }
284 },
285 "/snake": {
286 "get": {
287 "operationId": "snakeCaseOperation",
288 "tags": ["user_management"],
289 "responses": {
290 "200": {
291 "description": "snake_case tag"
292 }
293 }
294 }
295 },
296 "/screaming": {
297 "get": {
298 "operationId": "screamingCaseOperation",
299 "tags": ["USER_MANAGEMENT"],
300 "responses": {
301 "200": {
302 "description": "SCREAMING_SNAKE_CASE tag"
303 }
304 }
305 }
306 },
307 "/kebab": {
308 "get": {
309 "operationId": "kebabCaseOperation",
310 "tags": ["user-management"],
311 "responses": {
312 "200": {
313 "description": "kebab-case tag"
314 }
315 }
316 }
317 },
318 "/mixed": {
319 "get": {
320 "operationId": "mixedCaseOperation",
321 "tags": ["XMLHttpRequest", "HTTPSConnection", "APIKey"],
322 "responses": {
323 "200": {
324 "description": "Mixed case with acronyms"
325 }
326 }
327 }
328 }
329 }
330 });
331
332 Spec::from_value(spec_json).expect("Failed to create test spec")
333 }
334
335 fn create_test_spec_with_methods() -> Spec {
336 let spec_json = json!({
337 "openapi": "3.0.3",
338 "info": {
339 "title": "Test API with Multiple Methods",
340 "version": "1.0.0"
341 },
342 "paths": {
343 "/users": {
344 "get": {
345 "operationId": "listUsers",
346 "tags": ["user"],
347 "responses": {
348 "200": {
349 "description": "List of users"
350 }
351 }
352 },
353 "post": {
354 "operationId": "createUser",
355 "tags": ["user"],
356 "responses": {
357 "201": {
358 "description": "User created"
359 }
360 }
361 },
362 "put": {
363 "operationId": "updateUser",
364 "tags": ["user"],
365 "responses": {
366 "200": {
367 "description": "User updated"
368 }
369 }
370 },
371 "delete": {
372 "operationId": "deleteUser",
373 "tags": ["user"],
374 "responses": {
375 "204": {
376 "description": "User deleted"
377 }
378 }
379 }
380 },
381 "/pets": {
382 "get": {
383 "operationId": "listPets",
384 "tags": ["pet"],
385 "responses": {
386 "200": {
387 "description": "List of pets"
388 }
389 }
390 },
391 "post": {
392 "operationId": "createPet",
393 "tags": ["pet"],
394 "responses": {
395 "201": {
396 "description": "Pet created"
397 }
398 }
399 },
400 "patch": {
401 "operationId": "patchPet",
402 "tags": ["pet"],
403 "responses": {
404 "200": {
405 "description": "Pet patched"
406 }
407 }
408 }
409 },
410 "/health": {
411 "head": {
412 "operationId": "healthCheck",
413 "tags": ["health"],
414 "responses": {
415 "200": {
416 "description": "Health check"
417 }
418 }
419 },
420 "options": {
421 "operationId": "healthOptions",
422 "tags": ["health"],
423 "responses": {
424 "200": {
425 "description": "Health options"
426 }
427 }
428 }
429 }
430 }
431 });
432
433 Spec::from_value(spec_json).expect("Failed to create test spec")
434 }
435
436 #[test]
437 fn test_tag_filtering_no_filter() {
438 let spec = create_test_spec_with_tags();
439 let tools = spec
440 .to_tool_metadata(None, None)
441 .expect("Failed to generate tools");
442
443 assert_eq!(tools.len(), 5);
445
446 let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
447 assert!(tool_names.contains(&"listPets"));
448 assert!(tool_names.contains(&"createPet"));
449 assert!(tool_names.contains(&"listUsers"));
450 assert!(tool_names.contains(&"adminPanel"));
451 assert!(tool_names.contains(&"publicEndpoint"));
452 }
453
454 #[test]
455 fn test_tag_filtering_single_tag() {
456 let spec = create_test_spec_with_tags();
457 let filter_tags = vec!["pet".to_string()];
458 let tools = spec
459 .to_tool_metadata(Some(&filter_tags), None)
460 .expect("Failed to generate tools");
461
462 assert_eq!(tools.len(), 2);
464
465 let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
466 assert!(tool_names.contains(&"listPets"));
467 assert!(tool_names.contains(&"createPet"));
468 assert!(!tool_names.contains(&"listUsers"));
469 assert!(!tool_names.contains(&"adminPanel"));
470 assert!(!tool_names.contains(&"publicEndpoint"));
471 }
472
473 #[test]
474 fn test_tag_filtering_multiple_tags() {
475 let spec = create_test_spec_with_tags();
476 let filter_tags = vec!["pet".to_string(), "user".to_string()];
477 let tools = spec
478 .to_tool_metadata(Some(&filter_tags), None)
479 .expect("Failed to generate tools");
480
481 assert_eq!(tools.len(), 3);
483
484 let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
485 assert!(tool_names.contains(&"listPets"));
486 assert!(tool_names.contains(&"createPet"));
487 assert!(tool_names.contains(&"listUsers"));
488 assert!(!tool_names.contains(&"adminPanel"));
489 assert!(!tool_names.contains(&"publicEndpoint"));
490 }
491
492 #[test]
493 fn test_tag_filtering_or_logic() {
494 let spec = create_test_spec_with_tags();
495 let filter_tags = vec!["list".to_string()]; let tools = spec
497 .to_tool_metadata(Some(&filter_tags), None)
498 .expect("Failed to generate tools");
499
500 assert_eq!(tools.len(), 1);
502
503 let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
504 assert!(tool_names.contains(&"listPets")); assert!(!tool_names.contains(&"createPet")); }
507
508 #[test]
509 fn test_tag_filtering_no_matching_tags() {
510 let spec = create_test_spec_with_tags();
511 let filter_tags = vec!["nonexistent".to_string()];
512 let tools = spec
513 .to_tool_metadata(Some(&filter_tags), None)
514 .expect("Failed to generate tools");
515
516 assert_eq!(tools.len(), 0);
518 }
519
520 #[test]
521 fn test_tag_filtering_excludes_operations_without_tags() {
522 let spec = create_test_spec_with_tags();
523 let filter_tags = vec!["admin".to_string()];
524 let tools = spec
525 .to_tool_metadata(Some(&filter_tags), None)
526 .expect("Failed to generate tools");
527
528 assert_eq!(tools.len(), 1);
530
531 let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
532 assert!(tool_names.contains(&"adminPanel"));
533 assert!(!tool_names.contains(&"publicEndpoint")); }
535
536 #[test]
537 fn test_tag_normalization_all_cases_match() {
538 let spec = create_test_spec_with_mixed_case_tags();
539 let filter_tags = vec!["user-management".to_string()]; let tools = spec
541 .to_tool_metadata(Some(&filter_tags), None)
542 .expect("Failed to generate tools");
543
544 assert_eq!(tools.len(), 5);
546
547 let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
548 assert!(tool_names.contains(&"camelCaseOperation")); assert!(tool_names.contains(&"pascalCaseOperation")); assert!(tool_names.contains(&"snakeCaseOperation")); assert!(tool_names.contains(&"screamingCaseOperation")); assert!(tool_names.contains(&"kebabCaseOperation")); assert!(!tool_names.contains(&"mixedCaseOperation")); }
555
556 #[test]
557 fn test_tag_normalization_camel_case_filter() {
558 let spec = create_test_spec_with_mixed_case_tags();
559 let filter_tags = vec!["userManagement".to_string()]; let tools = spec
561 .to_tool_metadata(Some(&filter_tags), None)
562 .expect("Failed to generate tools");
563
564 assert_eq!(tools.len(), 5);
566
567 let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
568 assert!(tool_names.contains(&"camelCaseOperation"));
569 assert!(tool_names.contains(&"pascalCaseOperation"));
570 assert!(tool_names.contains(&"snakeCaseOperation"));
571 assert!(tool_names.contains(&"screamingCaseOperation"));
572 assert!(tool_names.contains(&"kebabCaseOperation"));
573 }
574
575 #[test]
576 fn test_tag_normalization_snake_case_filter() {
577 let spec = create_test_spec_with_mixed_case_tags();
578 let filter_tags = vec!["user_management".to_string()]; let tools = spec
580 .to_tool_metadata(Some(&filter_tags), None)
581 .expect("Failed to generate tools");
582
583 assert_eq!(tools.len(), 5);
585 }
586
587 #[test]
588 fn test_tag_normalization_acronyms() {
589 let spec = create_test_spec_with_mixed_case_tags();
590 let filter_tags = vec!["xml-http-request".to_string()]; let tools = spec
592 .to_tool_metadata(Some(&filter_tags), None)
593 .expect("Failed to generate tools");
594
595 assert_eq!(tools.len(), 1);
597
598 let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
599 assert!(tool_names.contains(&"mixedCaseOperation"));
600 }
601
602 #[test]
603 fn test_tag_normalization_multiple_mixed_filters() {
604 let spec = create_test_spec_with_mixed_case_tags();
605 let filter_tags = vec![
606 "user-management".to_string(), "HTTPSConnection".to_string(), ];
609 let tools = spec
610 .to_tool_metadata(Some(&filter_tags), None)
611 .expect("Failed to generate tools");
612
613 assert_eq!(tools.len(), 6);
615
616 let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
617 assert!(tool_names.contains(&"camelCaseOperation"));
618 assert!(tool_names.contains(&"pascalCaseOperation"));
619 assert!(tool_names.contains(&"snakeCaseOperation"));
620 assert!(tool_names.contains(&"screamingCaseOperation"));
621 assert!(tool_names.contains(&"kebabCaseOperation"));
622 assert!(tool_names.contains(&"mixedCaseOperation"));
623 }
624
625 #[test]
626 fn test_tag_filtering_empty_filter_list() {
627 let spec = create_test_spec_with_tags();
628 let filter_tags: Vec<String> = vec![];
629 let tools = spec
630 .to_tool_metadata(Some(&filter_tags), None)
631 .expect("Failed to generate tools");
632
633 assert_eq!(tools.len(), 0);
635 }
636
637 #[test]
638 fn test_tag_filtering_complex_scenario() {
639 let spec = create_test_spec_with_tags();
640 let filter_tags = vec!["management".to_string(), "list".to_string()];
641 let tools = spec
642 .to_tool_metadata(Some(&filter_tags), None)
643 .expect("Failed to generate tools");
644
645 assert_eq!(tools.len(), 2);
647
648 let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
649 assert!(tool_names.contains(&"adminPanel"));
650 assert!(tool_names.contains(&"listPets"));
651 assert!(!tool_names.contains(&"createPet"));
652 assert!(!tool_names.contains(&"listUsers"));
653 assert!(!tool_names.contains(&"publicEndpoint"));
654 }
655
656 #[test]
657 fn test_method_filtering_no_filter() {
658 let spec = create_test_spec_with_methods();
659 let tools = spec
660 .to_tool_metadata(None, None)
661 .expect("Failed to generate tools");
662
663 assert_eq!(tools.len(), 9);
665
666 let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
667 assert!(tool_names.contains(&"listUsers")); assert!(tool_names.contains(&"createUser")); assert!(tool_names.contains(&"updateUser")); assert!(tool_names.contains(&"deleteUser")); assert!(tool_names.contains(&"listPets")); assert!(tool_names.contains(&"createPet")); assert!(tool_names.contains(&"patchPet")); assert!(tool_names.contains(&"healthCheck")); assert!(tool_names.contains(&"healthOptions")); }
677
678 #[test]
679 fn test_method_filtering_single_method() {
680 use reqwest::Method;
681
682 let spec = create_test_spec_with_methods();
683 let filter_methods = vec![Method::GET];
684 let tools = spec
685 .to_tool_metadata(None, Some(&filter_methods))
686 .expect("Failed to generate tools");
687
688 assert_eq!(tools.len(), 2);
690
691 let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
692 assert!(tool_names.contains(&"listUsers")); assert!(tool_names.contains(&"listPets")); assert!(!tool_names.contains(&"createUser")); assert!(!tool_names.contains(&"updateUser")); assert!(!tool_names.contains(&"deleteUser")); assert!(!tool_names.contains(&"createPet")); assert!(!tool_names.contains(&"patchPet")); assert!(!tool_names.contains(&"healthCheck")); assert!(!tool_names.contains(&"healthOptions")); }
702
703 #[test]
704 fn test_method_filtering_multiple_methods() {
705 use reqwest::Method;
706
707 let spec = create_test_spec_with_methods();
708 let filter_methods = vec![Method::GET, Method::POST];
709 let tools = spec
710 .to_tool_metadata(None, Some(&filter_methods))
711 .expect("Failed to generate tools");
712
713 assert_eq!(tools.len(), 4);
715
716 let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
717 assert!(tool_names.contains(&"listUsers")); assert!(tool_names.contains(&"createUser")); assert!(tool_names.contains(&"listPets")); assert!(tool_names.contains(&"createPet")); assert!(!tool_names.contains(&"updateUser")); assert!(!tool_names.contains(&"deleteUser")); assert!(!tool_names.contains(&"patchPet")); assert!(!tool_names.contains(&"healthCheck")); assert!(!tool_names.contains(&"healthOptions")); }
727
728 #[test]
729 fn test_method_filtering_uncommon_methods() {
730 use reqwest::Method;
731
732 let spec = create_test_spec_with_methods();
733 let filter_methods = vec![Method::HEAD, Method::OPTIONS, Method::PATCH];
734 let tools = spec
735 .to_tool_metadata(None, Some(&filter_methods))
736 .expect("Failed to generate tools");
737
738 assert_eq!(tools.len(), 3);
740
741 let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
742 assert!(tool_names.contains(&"patchPet")); assert!(tool_names.contains(&"healthCheck")); assert!(tool_names.contains(&"healthOptions")); assert!(!tool_names.contains(&"listUsers")); assert!(!tool_names.contains(&"createUser")); assert!(!tool_names.contains(&"updateUser")); assert!(!tool_names.contains(&"deleteUser")); assert!(!tool_names.contains(&"listPets")); assert!(!tool_names.contains(&"createPet")); }
752
753 #[test]
754 fn test_method_and_tag_filtering_combined() {
755 use reqwest::Method;
756
757 let spec = create_test_spec_with_methods();
758 let filter_tags = vec!["user".to_string()];
759 let filter_methods = vec![Method::GET, Method::POST];
760 let tools = spec
761 .to_tool_metadata(Some(&filter_tags), Some(&filter_methods))
762 .expect("Failed to generate tools");
763
764 assert_eq!(tools.len(), 2);
766
767 let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
768 assert!(tool_names.contains(&"listUsers")); assert!(tool_names.contains(&"createUser")); assert!(!tool_names.contains(&"updateUser")); assert!(!tool_names.contains(&"deleteUser")); assert!(!tool_names.contains(&"listPets")); assert!(!tool_names.contains(&"createPet")); assert!(!tool_names.contains(&"patchPet")); assert!(!tool_names.contains(&"healthCheck")); assert!(!tool_names.contains(&"healthOptions")); }
778
779 #[test]
780 fn test_method_filtering_no_matching_methods() {
781 use reqwest::Method;
782
783 let spec = create_test_spec_with_methods();
784 let filter_methods = vec![Method::TRACE]; let tools = spec
786 .to_tool_metadata(None, Some(&filter_methods))
787 .expect("Failed to generate tools");
788
789 assert_eq!(tools.len(), 0);
791 }
792
793 #[test]
794 fn test_method_filtering_empty_filter_list() {
795 let spec = create_test_spec_with_methods();
796 let filter_methods: Vec<reqwest::Method> = vec![];
797 let tools = spec
798 .to_tool_metadata(None, Some(&filter_methods))
799 .expect("Failed to generate tools");
800
801 assert_eq!(tools.len(), 0);
803 }
804}