rmcp_openapi/
lib.rs

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