rmcp_openapi/
lib.rs

1pub mod error;
2pub mod http_client;
3pub mod openapi;
4pub mod server;
5pub mod tool_generator;
6pub mod tool_registry;
7
8pub use error::{CliError, OpenApiError, ToolCallError};
9pub use http_client::{HttpClient, HttpResponse};
10pub use openapi::OpenApiSpec;
11pub use openapi::OpenApiSpecLocation;
12pub use server::{OpenApiServer, ToolMetadata};
13pub use tool_generator::{ExtractedParameters, RequestConfig, ToolGenerator};
14pub use tool_registry::{ToolRegistry, ToolRegistryStats};
15
16/// Find similar strings using Jaro distance algorithm
17/// Used for parameter and tool name suggestions in errors
18pub(crate) fn find_similar_strings(unknown: &str, known_strings: &[&str]) -> Vec<String> {
19    use strsim::jaro;
20
21    let mut candidates = Vec::new();
22    for string in known_strings {
23        let confidence = jaro(unknown, string);
24        if confidence > 0.7 {
25            candidates.push((confidence, string.to_string()));
26        }
27    }
28
29    // Sort by confidence (highest first)
30    candidates.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap());
31    candidates.into_iter().map(|(_, name)| name).collect()
32}
33
34/// Normalize tag strings to kebab-case for consistent filtering
35/// Converts any case format (camelCase, PascalCase, snake_case, etc.) to kebab-case
36pub(crate) fn normalize_tag(tag: &str) -> String {
37    use heck::ToKebabCase;
38    tag.to_kebab_case()
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44
45    #[test]
46    fn test_find_similar_strings() {
47        // Test basic similarity
48        let known = vec!["page_size", "user_id", "status"];
49        let suggestions = find_similar_strings("page_sixe", &known);
50        assert_eq!(suggestions, vec!["page_size"]);
51
52        // Test no suggestions for very different string
53        let suggestions = find_similar_strings("xyz123", &known);
54        assert!(suggestions.is_empty());
55
56        // Test transposed characters
57        let known = vec!["limit", "offset"];
58        let suggestions = find_similar_strings("lmiit", &known);
59        assert_eq!(suggestions, vec!["limit"]);
60
61        // Test missing character
62        let known = vec!["project_id", "merge_request_id"];
63        let suggestions = find_similar_strings("projct_id", &known);
64        assert_eq!(suggestions, vec!["project_id"]);
65
66        // Test extra character
67        let known = vec!["name", "email"];
68        let suggestions = find_similar_strings("namee", &known);
69        assert_eq!(suggestions, vec!["name"]);
70    }
71
72    #[test]
73    fn test_normalize_tag() {
74        // Test camelCase conversion
75        assert_eq!(normalize_tag("userManagement"), "user-management");
76        assert_eq!(normalize_tag("getUsers"), "get-users");
77
78        // Test PascalCase conversion
79        assert_eq!(normalize_tag("UserManagement"), "user-management");
80        assert_eq!(normalize_tag("APIKey"), "api-key");
81
82        // Test snake_case conversion
83        assert_eq!(normalize_tag("user_management"), "user-management");
84        assert_eq!(normalize_tag("get_users"), "get-users");
85
86        // Test SCREAMING_SNAKE_CASE conversion
87        assert_eq!(normalize_tag("USER_MANAGEMENT"), "user-management");
88        assert_eq!(normalize_tag("API_KEY"), "api-key");
89
90        // Test already kebab-case
91        assert_eq!(normalize_tag("user-management"), "user-management");
92        assert_eq!(normalize_tag("api-key"), "api-key");
93
94        // Test single words
95        assert_eq!(normalize_tag("users"), "users");
96        assert_eq!(normalize_tag("API"), "api");
97
98        // Test empty string
99        assert_eq!(normalize_tag(""), "");
100
101        // Test edge cases
102        assert_eq!(normalize_tag("XMLHttpRequest"), "xml-http-request");
103        assert_eq!(normalize_tag("HTTPSConnection"), "https-connection");
104
105        // Test whitespace (heck handles spaces and trimming automatically)
106        assert_eq!(normalize_tag("user management"), "user-management");
107        assert_eq!(normalize_tag(" user "), "user"); // Leading/trailing spaces are trimmed
108
109        // Test multiple separators - heck handles these well
110        assert_eq!(normalize_tag("user__management"), "user-management");
111        assert_eq!(normalize_tag("user---management"), "user-management");
112
113        // Test special characters - heck handles these
114        assert_eq!(normalize_tag("user123Management"), "user123-management");
115        assert_eq!(normalize_tag("user@management"), "user-management"); // @ gets removed
116
117        // Test numbers and mixed content
118        assert_eq!(normalize_tag("v2ApiEndpoint"), "v2-api-endpoint");
119        assert_eq!(normalize_tag("HTML5Parser"), "html5-parser");
120    }
121}