rmcp_openapi/
spec.rs

1use crate::error::Error;
2use crate::normalize_tag;
3use crate::tool::ToolMetadata;
4use crate::tool_generator::ToolGenerator;
5use bon::Builder;
6use oas3::Spec as Oas3Spec;
7use reqwest::Method;
8use serde_json::Value;
9
10/// OpenAPI specification wrapper that provides convenience methods
11/// for working with oas3::Spec
12#[derive(Debug, Clone)]
13pub struct Spec {
14    pub spec: Oas3Spec,
15}
16
17impl Spec {
18    /// Parse an OpenAPI specification from a JSON value
19    pub fn from_value(json_value: Value) -> Result<Self, Error> {
20        let spec: Oas3Spec = serde_json::from_value(json_value)?;
21        Ok(Spec { spec })
22    }
23
24    /// Convert all operations to MCP tool metadata
25    pub fn to_tool_metadata(
26        &self,
27        filters: Option<&Filters>,
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                // Handle operations in the path item
36                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(filters) = filters {
50                            // Filter by methods if specified
51                            match &filters.methods {
52                                Some(Filter::Include(m)) if !m.contains(&method) => continue,
53                                Some(Filter::Exclude(m)) if m.contains(&method) => continue,
54                                _ => {}
55                            }
56
57                            // Filter by tags if specified (with kebab-case normalization)
58                            match (&filters.tags, operation.tags.is_empty()) {
59                                (Some(Filter::Include(tags)), false) => {
60                                    let normalized_filter_tags: Vec<String> =
61                                        tags.iter().map(|tag| normalize_tag(tag)).collect();
62
63                                    let has_matching_tag =
64                                        operation.tags.iter().any(|operation_tag| {
65                                            let normalized_operation_tag =
66                                                normalize_tag(operation_tag);
67                                            normalized_filter_tags
68                                                .contains(&normalized_operation_tag)
69                                        });
70
71                                    if !has_matching_tag {
72                                        continue; // Skip this operation
73                                    }
74                                }
75                                (Some(Filter::Exclude(tags)), false) => {
76                                    let normalized_filter_tags: Vec<String> =
77                                        tags.iter().map(|tag| normalize_tag(tag)).collect();
78
79                                    let has_matching_tag =
80                                        operation.tags.iter().any(|operation_tag| {
81                                            let normalized_operation_tag =
82                                                normalize_tag(operation_tag);
83                                            normalized_filter_tags
84                                                .contains(&normalized_operation_tag)
85                                        });
86
87                                    if has_matching_tag {
88                                        continue; // Skip this operation
89                                    }
90                                }
91                                (_, true) => continue, // Skip operations without tags when filtering
92                                _ => {}
93                            }
94
95                            // Filter by OperationId
96                            match (operation.operation_id.as_ref(), &filters.operations_id) {
97                                (Some(op), Some(Filter::Include(ops))) if !ops.contains(op) => {
98                                    continue;
99                                }
100                                (Some(op), Some(Filter::Exclude(ops))) if ops.contains(op) => {
101                                    continue;
102                                }
103                                _ => {}
104                            }
105                        }
106
107                        let tool_metadata = ToolGenerator::generate_tool_metadata(
108                            operation,
109                            method.to_string(),
110                            path.clone(),
111                            &self.spec,
112                            skip_tool_descriptions,
113                            skip_parameter_descriptions,
114                        )?;
115                        tools.push(tool_metadata);
116                    }
117                }
118            }
119        }
120
121        Ok(tools)
122    }
123
124    /// Convert all operations to OpenApiTool instances with HTTP configuration
125    ///
126    /// # Errors
127    ///
128    /// Returns an error if any operations cannot be converted or OpenApiTool instances cannot be created
129    pub fn to_openapi_tools(
130        &self,
131        filters: Option<&Filters>,
132        base_url: Option<url::Url>,
133        default_headers: Option<reqwest::header::HeaderMap>,
134        skip_tool_descriptions: bool,
135        skip_parameter_descriptions: bool,
136    ) -> Result<Vec<crate::tool::Tool>, Error> {
137        // First generate the tool metadata using existing method
138        let tools_metadata =
139            self.to_tool_metadata(filters, skip_tool_descriptions, skip_parameter_descriptions)?;
140
141        // Then convert to Tool instances
142        crate::tool_generator::ToolGenerator::generate_openapi_tools(
143            tools_metadata,
144            base_url,
145            default_headers,
146        )
147    }
148
149    /// Get operation by operation ID
150    pub fn get_operation(
151        &self,
152        operation_id: &str,
153    ) -> Option<(&oas3::spec::Operation, String, String)> {
154        if let Some(paths) = &self.spec.paths {
155            for (path, path_item) in paths {
156                let operations = [
157                    (Method::GET, &path_item.get),
158                    (Method::POST, &path_item.post),
159                    (Method::PUT, &path_item.put),
160                    (Method::DELETE, &path_item.delete),
161                    (Method::PATCH, &path_item.patch),
162                    (Method::HEAD, &path_item.head),
163                    (Method::OPTIONS, &path_item.options),
164                    (Method::TRACE, &path_item.trace),
165                ];
166
167                for (method, operation_ref) in operations {
168                    if let Some(operation) = operation_ref {
169                        let default_id = format!(
170                            "{}_{}",
171                            method,
172                            path.replace('/', "_").replace(['{', '}'], "")
173                        );
174                        let op_id = operation.operation_id.as_deref().unwrap_or(&default_id);
175
176                        if op_id == operation_id {
177                            return Some((operation, method.to_string(), path.clone()));
178                        }
179                    }
180                }
181            }
182        }
183        None
184    }
185
186    /// Get all operation IDs
187    pub fn get_operation_ids(&self) -> Vec<String> {
188        let mut operation_ids = Vec::new();
189
190        if let Some(paths) = &self.spec.paths {
191            for (path, path_item) in paths {
192                let operations = [
193                    (Method::GET, &path_item.get),
194                    (Method::POST, &path_item.post),
195                    (Method::PUT, &path_item.put),
196                    (Method::DELETE, &path_item.delete),
197                    (Method::PATCH, &path_item.patch),
198                    (Method::HEAD, &path_item.head),
199                    (Method::OPTIONS, &path_item.options),
200                    (Method::TRACE, &path_item.trace),
201                ];
202
203                for (method, operation_ref) in operations {
204                    if let Some(operation) = operation_ref {
205                        let default_id = format!(
206                            "{}_{}",
207                            method,
208                            path.replace('/', "_").replace(['{', '}'], "")
209                        );
210                        let op_id = operation.operation_id.as_deref().unwrap_or(&default_id);
211                        operation_ids.push(op_id.to_string());
212                    }
213                }
214            }
215        }
216
217        operation_ids
218    }
219}
220
221#[derive(Builder, Debug, Clone)]
222pub struct Filters {
223    pub tags: Option<Filter<String>>,
224    pub methods: Option<Filter<reqwest::Method>>,
225    pub operations_id: Option<Filter<String>>,
226}
227
228#[derive(Debug, Clone)]
229pub enum Filter<T> {
230    Include(Vec<T>),
231    Exclude(Vec<T>),
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use serde_json::json;
238
239    fn create_test_spec_with_tags() -> Spec {
240        let spec_json = json!({
241            "openapi": "3.0.3",
242            "info": {
243                "title": "Test API",
244                "version": "1.0.0"
245            },
246            "paths": {
247                "/pets": {
248                    "get": {
249                        "operationId": "listPets",
250                        "tags": ["pet", "list"],
251                        "responses": {
252                            "200": {
253                                "description": "List of pets"
254                            }
255                        }
256                    },
257                    "post": {
258                        "operationId": "createPet",
259                        "tags": ["pet"],
260                        "responses": {
261                            "201": {
262                                "description": "Pet created"
263                            }
264                        }
265                    }
266                },
267                "/users": {
268                    "get": {
269                        "operationId": "listUsers",
270                        "tags": ["user"],
271                        "responses": {
272                            "200": {
273                                "description": "List of users"
274                            }
275                        }
276                    }
277                },
278                "/admin": {
279                    "get": {
280                        "operationId": "adminPanel",
281                        "tags": ["admin", "management"],
282                        "responses": {
283                            "200": {
284                                "description": "Admin panel"
285                            }
286                        }
287                    }
288                },
289                "/public": {
290                    "get": {
291                        "operationId": "publicEndpoint",
292                        "responses": {
293                            "200": {
294                                "description": "Public endpoint with no tags"
295                            }
296                        }
297                    }
298                }
299            }
300        });
301
302        Spec::from_value(spec_json).expect("Failed to create test spec")
303    }
304
305    fn create_test_spec_with_mixed_case_tags() -> Spec {
306        let spec_json = json!({
307            "openapi": "3.0.3",
308            "info": {
309                "title": "Test API with Mixed Case Tags",
310                "version": "1.0.0"
311            },
312            "paths": {
313                "/camel": {
314                    "get": {
315                        "operationId": "camelCaseOperation",
316                        "tags": ["userManagement"],
317                        "responses": {
318                            "200": {
319                                "description": "camelCase tag"
320                            }
321                        }
322                    }
323                },
324                "/pascal": {
325                    "get": {
326                        "operationId": "pascalCaseOperation",
327                        "tags": ["UserManagement"],
328                        "responses": {
329                            "200": {
330                                "description": "PascalCase tag"
331                            }
332                        }
333                    }
334                },
335                "/snake": {
336                    "get": {
337                        "operationId": "snakeCaseOperation",
338                        "tags": ["user_management"],
339                        "responses": {
340                            "200": {
341                                "description": "snake_case tag"
342                            }
343                        }
344                    }
345                },
346                "/screaming": {
347                    "get": {
348                        "operationId": "screamingCaseOperation",
349                        "tags": ["USER_MANAGEMENT"],
350                        "responses": {
351                            "200": {
352                                "description": "SCREAMING_SNAKE_CASE tag"
353                            }
354                        }
355                    }
356                },
357                "/kebab": {
358                    "get": {
359                        "operationId": "kebabCaseOperation",
360                        "tags": ["user-management"],
361                        "responses": {
362                            "200": {
363                                "description": "kebab-case tag"
364                            }
365                        }
366                    }
367                },
368                "/mixed": {
369                    "get": {
370                        "operationId": "mixedCaseOperation",
371                        "tags": ["XMLHttpRequest", "HTTPSConnection", "APIKey"],
372                        "responses": {
373                            "200": {
374                                "description": "Mixed case with acronyms"
375                            }
376                        }
377                    }
378                }
379            }
380        });
381
382        Spec::from_value(spec_json).expect("Failed to create test spec")
383    }
384
385    fn create_test_spec_with_methods() -> Spec {
386        let spec_json = json!({
387            "openapi": "3.0.3",
388            "info": {
389                "title": "Test API with Multiple Methods",
390                "version": "1.0.0"
391            },
392            "paths": {
393                "/users": {
394                    "get": {
395                        "operationId": "listUsers",
396                        "tags": ["user"],
397                        "responses": {
398                            "200": {
399                                "description": "List of users"
400                            }
401                        }
402                    },
403                    "post": {
404                        "operationId": "createUser",
405                        "tags": ["user"],
406                        "responses": {
407                            "201": {
408                                "description": "User created"
409                            }
410                        }
411                    },
412                    "put": {
413                        "operationId": "updateUser",
414                        "tags": ["user"],
415                        "responses": {
416                            "200": {
417                                "description": "User updated"
418                            }
419                        }
420                    },
421                    "delete": {
422                        "operationId": "deleteUser",
423                        "tags": ["user"],
424                        "responses": {
425                            "204": {
426                                "description": "User deleted"
427                            }
428                        }
429                    }
430                },
431                "/pets": {
432                    "get": {
433                        "operationId": "listPets",
434                        "tags": ["pet"],
435                        "responses": {
436                            "200": {
437                                "description": "List of pets"
438                            }
439                        }
440                    },
441                    "post": {
442                        "operationId": "createPet",
443                        "tags": ["pet"],
444                        "responses": {
445                            "201": {
446                                "description": "Pet created"
447                            }
448                        }
449                    },
450                    "patch": {
451                        "operationId": "patchPet",
452                        "tags": ["pet"],
453                        "responses": {
454                            "200": {
455                                "description": "Pet patched"
456                            }
457                        }
458                    }
459                },
460                "/health": {
461                    "head": {
462                        "operationId": "healthCheck",
463                        "tags": ["health"],
464                        "responses": {
465                            "200": {
466                                "description": "Health check"
467                            }
468                        }
469                    },
470                    "options": {
471                        "operationId": "healthOptions",
472                        "tags": ["health"],
473                        "responses": {
474                            "200": {
475                                "description": "Health options"
476                            }
477                        }
478                    }
479                }
480            }
481        });
482
483        Spec::from_value(spec_json).expect("Failed to create test spec")
484    }
485
486    #[test]
487    fn test_tag_filtering_no_filter() {
488        let spec = create_test_spec_with_tags();
489        let tools = spec
490            .to_tool_metadata(None, false, false)
491            .expect("Failed to generate tools");
492
493        // All operations should be included
494        assert_eq!(tools.len(), 5);
495
496        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
497        assert!(tool_names.contains(&"listPets"));
498        assert!(tool_names.contains(&"createPet"));
499        assert!(tool_names.contains(&"listUsers"));
500        assert!(tool_names.contains(&"adminPanel"));
501        assert!(tool_names.contains(&"publicEndpoint"));
502    }
503
504    #[test]
505    fn test_tag_filtering_single_tag() {
506        let spec = create_test_spec_with_tags();
507        let filters = Some(
508            Filters::builder()
509                .tags(Filter::Include(vec!["pet".to_string()]))
510                .build(),
511        );
512        let tools = spec
513            .to_tool_metadata(filters.as_ref(), false, false)
514            .expect("Failed to generate tools");
515
516        // Only pet operations should be included
517        assert_eq!(tools.len(), 2);
518
519        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
520        assert!(tool_names.contains(&"listPets"));
521        assert!(tool_names.contains(&"createPet"));
522        assert!(!tool_names.contains(&"listUsers"));
523        assert!(!tool_names.contains(&"adminPanel"));
524        assert!(!tool_names.contains(&"publicEndpoint"));
525    }
526
527    #[test]
528    fn test_tag_filtering_multiple_tags() {
529        let spec = create_test_spec_with_tags();
530        let filters = Some(
531            Filters::builder()
532                .tags(Filter::Include(vec!["pet".to_string(), "user".to_string()]))
533                .build(),
534        );
535        let tools = spec
536            .to_tool_metadata(filters.as_ref(), false, false)
537            .expect("Failed to generate tools");
538
539        // Pet and user operations should be included
540        assert_eq!(tools.len(), 3);
541
542        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
543        assert!(tool_names.contains(&"listPets"));
544        assert!(tool_names.contains(&"createPet"));
545        assert!(tool_names.contains(&"listUsers"));
546        assert!(!tool_names.contains(&"adminPanel"));
547        assert!(!tool_names.contains(&"publicEndpoint"));
548    }
549
550    #[test]
551    fn test_tag_filtering_or_logic() {
552        let spec = create_test_spec_with_tags();
553        let filters = Some(
554            Filters::builder()
555                .tags(Filter::Include(vec!["list".to_string()]))
556                .build(),
557        );
558        let tools = spec
559            .to_tool_metadata(filters.as_ref(), false, false)
560            .expect("Failed to generate tools");
561
562        // Only operations with "list" tag should be included
563        assert_eq!(tools.len(), 1);
564
565        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
566        assert!(tool_names.contains(&"listPets")); // Has both "pet" and "list" tags
567        assert!(!tool_names.contains(&"createPet")); // Only has "pet" tag
568    }
569
570    #[test]
571    fn test_tag_filtering_no_matching_tags() {
572        let spec = create_test_spec_with_tags();
573        let filters = Some(
574            Filters::builder()
575                .tags(Filter::Include(vec!["nonexistent".to_string()]))
576                .build(),
577        );
578        let tools = spec
579            .to_tool_metadata(filters.as_ref(), false, false)
580            .expect("Failed to generate tools");
581
582        // No operations should be included
583        assert_eq!(tools.len(), 0);
584    }
585
586    #[test]
587    fn test_tag_filtering_excludes_operations_without_tags() {
588        let spec = create_test_spec_with_tags();
589        let filters = Some(
590            Filters::builder()
591                .tags(Filter::Include(vec!["admin".to_string()]))
592                .build(),
593        );
594        let tools = spec
595            .to_tool_metadata(filters.as_ref(), false, false)
596            .expect("Failed to generate tools");
597
598        // Only admin operations should be included, public endpoint (no tags) should be excluded
599        assert_eq!(tools.len(), 1);
600
601        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
602        assert!(tool_names.contains(&"adminPanel"));
603        assert!(!tool_names.contains(&"publicEndpoint")); // No tags, should be excluded
604    }
605
606    #[test]
607    fn test_tag_normalization_all_cases_match() {
608        let spec = create_test_spec_with_mixed_case_tags();
609        let filters = Some(
610            Filters::builder()
611                .tags(Filter::Include(vec!["user-management".to_string()]))
612                .build(),
613        );
614        let tools = spec
615            .to_tool_metadata(filters.as_ref(), false, false)
616            .expect("Failed to generate tools");
617
618        // All userManagement variants should match user-management filter
619        assert_eq!(tools.len(), 5);
620
621        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
622        assert!(tool_names.contains(&"camelCaseOperation")); // userManagement
623        assert!(tool_names.contains(&"pascalCaseOperation")); // UserManagement
624        assert!(tool_names.contains(&"snakeCaseOperation")); // user_management
625        assert!(tool_names.contains(&"screamingCaseOperation")); // USER_MANAGEMENT
626        assert!(tool_names.contains(&"kebabCaseOperation")); // user-management
627        assert!(!tool_names.contains(&"mixedCaseOperation")); // Different tags
628    }
629
630    #[test]
631    fn test_tag_normalization_camel_case_filter() {
632        let spec = create_test_spec_with_mixed_case_tags();
633        let filters = Some(
634            Filters::builder()
635                .tags(Filter::Include(vec!["userManagement".to_string()]))
636                .build(),
637        );
638        let tools = spec
639            .to_tool_metadata(filters.as_ref(), false, false)
640            .expect("Failed to generate tools");
641
642        // All userManagement variants should match camelCase filter
643        assert_eq!(tools.len(), 5);
644
645        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
646        assert!(tool_names.contains(&"camelCaseOperation"));
647        assert!(tool_names.contains(&"pascalCaseOperation"));
648        assert!(tool_names.contains(&"snakeCaseOperation"));
649        assert!(tool_names.contains(&"screamingCaseOperation"));
650        assert!(tool_names.contains(&"kebabCaseOperation"));
651    }
652
653    #[test]
654    fn test_tag_normalization_snake_case_filter() {
655        let spec = create_test_spec_with_mixed_case_tags();
656        let filters = Some(
657            Filters::builder()
658                .tags(Filter::Include(vec!["user_management".to_string()]))
659                .build(),
660        );
661        let tools = spec
662            .to_tool_metadata(filters.as_ref(), false, false)
663            .expect("Failed to generate tools");
664
665        // All userManagement variants should match snake_case filter
666        assert_eq!(tools.len(), 5);
667    }
668
669    #[test]
670    fn test_tag_normalization_acronyms() {
671        let spec = create_test_spec_with_mixed_case_tags();
672        let filters = Some(
673            Filters::builder()
674                .tags(Filter::Include(vec!["xml-http-request".to_string()]))
675                .build(),
676        );
677        let tools = spec
678            .to_tool_metadata(filters.as_ref(), false, false)
679            .expect("Failed to generate tools");
680
681        // Should match XMLHttpRequest tag
682        assert_eq!(tools.len(), 1);
683
684        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
685        assert!(tool_names.contains(&"mixedCaseOperation"));
686    }
687
688    #[test]
689    fn test_tag_normalization_multiple_mixed_filters() {
690        let spec = create_test_spec_with_mixed_case_tags();
691        let filters = Some(
692            Filters::builder()
693                .tags(Filter::Include(vec![
694                    "user-management".to_string(),
695                    "HTTPSConnection".to_string(),
696                ]))
697                .build(),
698        );
699        let tools = spec
700            .to_tool_metadata(filters.as_ref(), false, false)
701            .expect("Failed to generate tools");
702
703        // Should match all userManagement variants + mixedCaseOperation (for HTTPSConnection)
704        assert_eq!(tools.len(), 6);
705
706        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
707        assert!(tool_names.contains(&"camelCaseOperation"));
708        assert!(tool_names.contains(&"pascalCaseOperation"));
709        assert!(tool_names.contains(&"snakeCaseOperation"));
710        assert!(tool_names.contains(&"screamingCaseOperation"));
711        assert!(tool_names.contains(&"kebabCaseOperation"));
712        assert!(tool_names.contains(&"mixedCaseOperation"));
713    }
714
715    #[test]
716    fn test_tag_filtering_empty_filter_list() {
717        let spec = create_test_spec_with_tags();
718        let filters = Some(Filters::builder().tags(Filter::Include(vec![])).build());
719        let tools = spec
720            .to_tool_metadata(filters.as_ref(), false, false)
721            .expect("Failed to generate tools");
722
723        // Empty filter should exclude all operations
724        dbg!(&tools);
725        assert_eq!(tools.len(), 0);
726    }
727
728    #[test]
729    fn test_tag_filtering_complex_scenario() {
730        let spec = create_test_spec_with_tags();
731        let filters = Some(
732            Filters::builder()
733                .tags(Filter::Include(vec![
734                    "management".to_string(),
735                    "list".to_string(),
736                ]))
737                .build(),
738        );
739        let tools = spec
740            .to_tool_metadata(filters.as_ref(), false, false)
741            .expect("Failed to generate tools");
742
743        // Should include adminPanel (has "management") and listPets (has "list")
744        assert_eq!(tools.len(), 2);
745
746        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
747        assert!(tool_names.contains(&"adminPanel"));
748        assert!(tool_names.contains(&"listPets"));
749        assert!(!tool_names.contains(&"createPet"));
750        assert!(!tool_names.contains(&"listUsers"));
751        assert!(!tool_names.contains(&"publicEndpoint"));
752    }
753
754    #[test]
755    fn test_method_filtering_no_filter() {
756        let spec = create_test_spec_with_methods();
757        let tools = spec
758            .to_tool_metadata(None, false, false)
759            .expect("Failed to generate tools");
760
761        // All operations should be included (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)
762        assert_eq!(tools.len(), 9);
763
764        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
765        assert!(tool_names.contains(&"listUsers")); // GET /users
766        assert!(tool_names.contains(&"createUser")); // POST /users
767        assert!(tool_names.contains(&"updateUser")); // PUT /users
768        assert!(tool_names.contains(&"deleteUser")); // DELETE /users
769        assert!(tool_names.contains(&"listPets")); // GET /pets
770        assert!(tool_names.contains(&"createPet")); // POST /pets
771        assert!(tool_names.contains(&"patchPet")); // PATCH /pets
772        assert!(tool_names.contains(&"healthCheck")); // HEAD /health
773        assert!(tool_names.contains(&"healthOptions")); // OPTIONS /health
774    }
775
776    #[test]
777    fn test_method_filtering_single_method() {
778        use reqwest::Method;
779
780        let spec = create_test_spec_with_methods();
781        let filters = Some(
782            Filters::builder()
783                .methods(Filter::Include(vec![Method::GET]))
784                .build(),
785        );
786        let tools = spec
787            .to_tool_metadata(filters.as_ref(), false, false)
788            .expect("Failed to generate tools");
789
790        // Only GET operations should be included
791        assert_eq!(tools.len(), 2);
792
793        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
794        assert!(tool_names.contains(&"listUsers")); // GET /users
795        assert!(tool_names.contains(&"listPets")); // GET /pets
796        assert!(!tool_names.contains(&"createUser")); // POST /users
797        assert!(!tool_names.contains(&"updateUser")); // PUT /users
798        assert!(!tool_names.contains(&"deleteUser")); // DELETE /users
799        assert!(!tool_names.contains(&"createPet")); // POST /pets
800        assert!(!tool_names.contains(&"patchPet")); // PATCH /pets
801        assert!(!tool_names.contains(&"healthCheck")); // HEAD /health
802        assert!(!tool_names.contains(&"healthOptions")); // OPTIONS /health
803    }
804
805    #[test]
806    fn test_method_filtering_multiple_methods() {
807        use reqwest::Method;
808
809        let spec = create_test_spec_with_methods();
810        let filters = Some(
811            Filters::builder()
812                .methods(Filter::Include(vec![Method::GET, Method::POST]))
813                .build(),
814        );
815        let tools = spec
816            .to_tool_metadata(filters.as_ref(), false, false)
817            .expect("Failed to generate tools");
818
819        // Only GET and POST operations should be included
820        assert_eq!(tools.len(), 4);
821
822        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
823        assert!(tool_names.contains(&"listUsers")); // GET /users
824        assert!(tool_names.contains(&"createUser")); // POST /users
825        assert!(tool_names.contains(&"listPets")); // GET /pets
826        assert!(tool_names.contains(&"createPet")); // POST /pets
827        assert!(!tool_names.contains(&"updateUser")); // PUT /users
828        assert!(!tool_names.contains(&"deleteUser")); // DELETE /users
829        assert!(!tool_names.contains(&"patchPet")); // PATCH /pets
830        assert!(!tool_names.contains(&"healthCheck")); // HEAD /health
831        assert!(!tool_names.contains(&"healthOptions")); // OPTIONS /health
832    }
833
834    #[test]
835    fn test_method_filtering_uncommon_methods() {
836        use reqwest::Method;
837
838        let spec = create_test_spec_with_methods();
839        let filters = Some(
840            Filters::builder()
841                .methods(Filter::Include(vec![
842                    Method::HEAD,
843                    Method::OPTIONS,
844                    Method::PATCH,
845                ]))
846                .build(),
847        );
848        let tools = spec
849            .to_tool_metadata(filters.as_ref(), false, false)
850            .expect("Failed to generate tools");
851
852        // Only HEAD, OPTIONS, and PATCH operations should be included
853        assert_eq!(tools.len(), 3);
854
855        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
856        assert!(tool_names.contains(&"patchPet")); // PATCH /pets
857        assert!(tool_names.contains(&"healthCheck")); // HEAD /health
858        assert!(tool_names.contains(&"healthOptions")); // OPTIONS /health
859        assert!(!tool_names.contains(&"listUsers")); // GET /users
860        assert!(!tool_names.contains(&"createUser")); // POST /users
861        assert!(!tool_names.contains(&"updateUser")); // PUT /users
862        assert!(!tool_names.contains(&"deleteUser")); // DELETE /users
863        assert!(!tool_names.contains(&"listPets")); // GET /pets
864        assert!(!tool_names.contains(&"createPet")); // POST /pets
865    }
866
867    #[test]
868    fn test_method_and_tag_filtering_combined() {
869        use reqwest::Method;
870
871        let spec = create_test_spec_with_methods();
872        let filters = Some(
873            Filters::builder()
874                .tags(Filter::Include(vec!["user".to_string()]))
875                .methods(Filter::Include(vec![Method::GET, Method::POST]))
876                .build(),
877        );
878        let tools = spec
879            .to_tool_metadata(filters.as_ref(), false, false)
880            .expect("Failed to generate tools");
881
882        // Only user operations with GET and POST methods should be included
883        assert_eq!(tools.len(), 2);
884
885        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
886        assert!(tool_names.contains(&"listUsers")); // GET /users (has user tag)
887        assert!(tool_names.contains(&"createUser")); // POST /users (has user tag)
888        assert!(!tool_names.contains(&"updateUser")); // PUT /users (user tag but not GET/POST)
889        assert!(!tool_names.contains(&"deleteUser")); // DELETE /users (user tag but not GET/POST)
890        assert!(!tool_names.contains(&"listPets")); // GET /pets (GET method but not user tag)
891        assert!(!tool_names.contains(&"createPet")); // POST /pets (POST method but not user tag)
892        assert!(!tool_names.contains(&"patchPet")); // PATCH /pets (neither user tag nor GET/POST)
893        assert!(!tool_names.contains(&"healthCheck")); // HEAD /health (neither user tag nor GET/POST)
894        assert!(!tool_names.contains(&"healthOptions")); // OPTIONS /health (neither user tag nor GET/POST)
895    }
896
897    #[test]
898    fn test_method_filtering_no_matching_methods() {
899        use reqwest::Method;
900
901        let spec = create_test_spec_with_methods();
902        let filters = Some(
903            Filters::builder()
904                .methods(Filter::Include(vec![Method::TRACE]))
905                .build(),
906        );
907        let tools = spec
908            .to_tool_metadata(filters.as_ref(), false, false)
909            .expect("Failed to generate tools");
910
911        // No operations should be included
912        assert_eq!(tools.len(), 0);
913    }
914
915    #[test]
916    fn test_method_filtering_empty_filter_list() {
917        let spec = create_test_spec_with_methods();
918        let filters = Some(Filters::builder().methods(Filter::Include(vec![])).build());
919        let tools = spec
920            .to_tool_metadata(filters.as_ref(), false, false)
921            .expect("Failed to generate tools");
922
923        // Empty filter should exclude all operations
924        assert_eq!(tools.len(), 0);
925    }
926
927    #[test]
928    fn test_operations_include_filter_empty_filter_list() {
929        let spec = create_test_spec_with_methods();
930        let filters = Some(Filters::builder().methods(Filter::Include(vec![])).build());
931        let tools = spec
932            .to_tool_metadata(filters.as_ref(), false, false)
933            .expect("Failed to generate tools");
934
935        // Empty include filter should exclude all operations
936        assert_eq!(tools.len(), 0);
937    }
938
939    #[test]
940    fn test_operations_include_filter_two_operations_filter_list() {
941        let spec = create_test_spec_with_methods();
942        let filters = Some(
943            Filters::builder()
944                .operations_id(Filter::Include(vec![
945                    "listUsers".to_owned(),
946                    "patchPet".to_owned(),
947                ]))
948                .build(),
949        );
950        let tools = spec
951            .to_tool_metadata(filters.as_ref(), false, false)
952            .expect("Failed to generate tools");
953
954        assert_eq!(tools.len(), 2);
955
956        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
957        assert!(tool_names.contains(&"listUsers")); // GET /users (has user tag)
958        assert!(tool_names.contains(&"patchPet")); // POST /users (has user tag)
959    }
960
961    #[test]
962    fn test_operations_exclude_filter_empty_filter_list() {
963        let spec = create_test_spec_with_methods();
964        let filters = Some(
965            Filters::builder()
966                .operations_id(Filter::Exclude(vec![]))
967                .build(),
968        );
969        let tools = spec
970            .to_tool_metadata(filters.as_ref(), false, false)
971            .expect("Failed to generate tools");
972
973        // Empty include filter should exclude all operations
974        assert_eq!(tools.len(), 9);
975    }
976
977    #[test]
978    fn test_operations_exclude_filter_three_operations_filter_list() {
979        let spec = create_test_spec_with_methods();
980        let filters = Some(
981            Filters::builder()
982                .operations_id(Filter::Exclude(vec![
983                    "createUser".to_owned(),
984                    "deleteUser".to_owned(),
985                    "healthCheck".to_owned(),
986                ]))
987                .build(),
988        );
989        let tools = spec
990            .to_tool_metadata(filters.as_ref(), false, false)
991            .expect("Failed to generate tools");
992
993        assert_eq!(tools.len(), 6);
994
995        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
996        assert!(tool_names.contains(&"listUsers"));
997        assert!(tool_names.contains(&"updateUser"));
998        assert!(tool_names.contains(&"listPets"));
999        assert!(tool_names.contains(&"createPet"));
1000        assert!(tool_names.contains(&"patchPet"));
1001        assert!(tool_names.contains(&"healthOptions"))
1002    }
1003
1004    #[test]
1005    fn test_all_filters_combined_1() {
1006        let spec = create_test_spec_with_tags();
1007        let filters = Some(
1008            Filters::builder()
1009                .tags(Filter::Include(vec![
1010                    "pet".to_owned(),
1011                    "user".to_owned(),
1012                    "admin".to_owned(),
1013                ]))
1014                .methods(Filter::Include(vec![Method::GET, Method::POST]))
1015                .operations_id(Filter::Exclude(vec![
1016                    "listPets".to_owned(),
1017                    "createPet".to_owned(),
1018                    "publicEndpoint".to_owned(),
1019                ]))
1020                .build(),
1021        );
1022        let tools = spec
1023            .to_tool_metadata(filters.as_ref(), false, false)
1024            .expect("Failed to generate tools");
1025
1026        assert_eq!(tools.len(), 2);
1027
1028        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
1029
1030        assert!(tool_names.contains(&"listUsers"));
1031        assert!(tool_names.contains(&"adminPanel"));
1032    }
1033
1034    #[test]
1035    fn test_all_filters_combined_2() {
1036        let spec = create_test_spec_with_methods();
1037        let filters = Some(
1038            Filters::builder()
1039                .tags(Filter::Exclude(vec!["health".to_owned()]))
1040                .methods(Filter::Exclude(vec![Method::GET, Method::POST]))
1041                .operations_id(Filter::Include(vec![
1042                    "listUsers".to_owned(),
1043                    "updateUser".to_owned(),
1044                    "deleteUser".to_owned(),
1045                    "listPets".to_owned(),
1046                    "patchPet".to_owned(),
1047                    "healthCheck".to_owned(),
1048                    "healthOptions".to_owned(),
1049                ]))
1050                .build(),
1051        );
1052        let tools = spec
1053            .to_tool_metadata(filters.as_ref(), false, false)
1054            .expect("Failed to generate tools");
1055
1056        assert_eq!(tools.len(), 3);
1057
1058        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
1059
1060        assert!(tool_names.contains(&"updateUser"));
1061        assert!(tool_names.contains(&"deleteUser"));
1062        assert!(tool_names.contains(&"patchPet"));
1063    }
1064}