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