1pub fn sanitize_tool_name(tool_id: &str) -> String {
23 tool_id
24 .chars()
25 .map(|c| {
26 if c.is_ascii_alphanumeric() || c == '_' || c == '-' {
27 c
28 } else {
29 '_'
30 }
31 })
32 .collect()
33}
34
35pub fn desanitize_tool_name<'a, I>(sanitized: &str, known: I) -> Option<&'a str>
42where
43 I: IntoIterator<Item = &'a str>,
44{
45 known
46 .into_iter()
47 .find(|id| sanitize_tool_name(id) == sanitized)
48}
49
50pub fn detect_collisions<'a, I>(ids: I) -> Vec<(String, Vec<&'a str>)>
54where
55 I: IntoIterator<Item = &'a str>,
56{
57 let mut groups: std::collections::HashMap<String, Vec<&'a str>> =
58 std::collections::HashMap::new();
59 for id in ids {
60 groups.entry(sanitize_tool_name(id)).or_default().push(id);
61 }
62 groups.into_iter().filter(|(_, v)| v.len() > 1).collect()
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[test]
70 fn plain_ascii_passes_through() {
71 assert_eq!(sanitize_tool_name("echo_say"), "echo_say");
72 assert_eq!(sanitize_tool_name("tool-name"), "tool-name");
73 }
74
75 #[test]
76 fn colon_and_dot_become_underscore() {
77 assert_eq!(sanitize_tool_name("ref:fs.read"), "ref_fs_read");
78 assert_eq!(
79 sanitize_tool_name("xiaomi:light.toggle"),
80 "xiaomi_light_toggle"
81 );
82 }
83
84 #[test]
85 fn multiple_special_chars() {
86 assert_eq!(sanitize_tool_name("a/b c+d"), "a_b_c_d");
87 }
88
89 #[test]
90 fn desanitize_round_trips_via_known_list() {
91 let known = &["ref:fs.read", "ref:shell.exec", "ref:echo.say"];
92 let hit = desanitize_tool_name("ref_shell_exec", known.iter().copied());
93 assert_eq!(hit, Some("ref:shell.exec"));
94 }
95
96 #[test]
97 fn desanitize_returns_none_for_unknown() {
98 let known = &["ref:fs.read"];
99 assert!(desanitize_tool_name("something_else", known.iter().copied()).is_none());
100 }
101
102 #[test]
103 fn collisions_are_detected() {
104 let ids = &["a:b", "a.b", "a_b", "c:d"];
105 let collisions = detect_collisions(ids.iter().copied());
106 assert_eq!(collisions.len(), 1);
107 let (sanitized, members) = &collisions[0];
108 assert_eq!(sanitized, "a_b");
109 assert_eq!(members.len(), 3);
110 }
111}