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}