SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
e5034c3 verified
Raw History Blame Contribute Delete
7.45 kB
use std::fmt::Display;
use regex::Regex;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
pub struct ToolName(String);
impl ToolName {
pub fn new(value: impl ToString) -> Self {
ToolName(value.to_string())
}
/// Transforms the tool_name to remove whitespaces and converts to
/// lower_snake_case
pub fn sanitized(input: &str) -> Self {
// Convert to lowercase
let input = input.to_lowercase();
// Replace all non-alphanumeric characters (excluding underscore) with
// underscores
let re_special = Regex::new(r"[^a-z0-9_]+").unwrap();
let cleaned = re_special.replace_all(&input, "_");
// Remove leading/trailing underscores and collapse consecutive underscores
let re_trimmed = Regex::new(r"_+").unwrap();
let sanitized_str = re_trimmed
.replace_all(&cleaned, "_")
.trim_matches('_')
.to_string();
Self(sanitized_str)
}
}
impl ToolName {
pub fn into_string(self) -> String {
self.0
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_sanitized(self) -> Self {
ToolName::sanitized(self.0.as_str())
}
/// Converts a Claude Code format MCP tool name (`mcp__{server}__{tool}`) to
/// Forge's internal legacy format (`mcp_{server}_tool_{tool}`).
///
/// Returns `None` if the name does not match the Claude Code MCP format.
/// Sanitized names never contain `__`, so the first `__` is always the
/// server/tool separator.
pub fn to_legacy_mcp_name(&self) -> Option<ToolName> {
let rest = self.0.strip_prefix("mcp__")?;
let (server, tool) = rest.split_once("__")?;
Some(ToolName::new(format!("mcp_{server}_tool_{tool}")))
}
}
impl From<String> for ToolName {
fn from(value: String) -> Self {
ToolName::new(value)
}
}
impl From<&str> for ToolName {
fn from(value: &str) -> Self {
ToolName::new(value)
}
}
pub trait NamedTool {
fn tool_name() -> ToolName;
}
impl Display for ToolName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_sanitize_camel_case() {
let tool_name = ToolName::new("camelCase");
let actual = tool_name.into_sanitized();
let expected = ToolName::new("camelcase");
assert_eq!(actual, expected);
}
#[test]
fn test_sanitize_pascal_case() {
let tool_name = ToolName::new("PascalCase");
let actual = tool_name.into_sanitized();
let expected = ToolName::new("pascalcase");
assert_eq!(actual, expected);
}
#[test]
fn test_sanitize_mixed_case_with_numbers() {
let tool_name = ToolName::new("myTool2Name");
let actual = tool_name.into_sanitized();
let expected = ToolName::new("mytool2name");
assert_eq!(actual, expected);
}
#[test]
fn test_sanitize_special_characters() {
let tool_name = ToolName::new("tool-name@with#special$chars");
let actual = tool_name.into_sanitized();
let expected = ToolName::new("tool_name_with_special_chars");
assert_eq!(actual, expected);
}
#[test]
fn test_sanitize_whitespace() {
let tool_name = ToolName::new("tool name with spaces");
let actual = tool_name.into_sanitized();
let expected = ToolName::new("tool_name_with_spaces");
assert_eq!(actual, expected);
}
#[test]
fn test_sanitize_consecutive_special_chars() {
let tool_name = ToolName::new("tool---name___with@@@special");
let actual = tool_name.into_sanitized();
let expected = ToolName::new("tool_name_with_special");
assert_eq!(actual, expected);
}
#[test]
fn test_sanitize_leading_trailing_special_chars() {
let tool_name = ToolName::new("___tool_name___");
let actual = tool_name.into_sanitized();
let expected = ToolName::new("tool_name");
assert_eq!(actual, expected);
}
#[test]
fn test_sanitize_already_snake_case() {
let tool_name = ToolName::new("already_snake_case");
let actual = tool_name.into_sanitized();
let expected = ToolName::new("already_snake_case");
assert_eq!(actual, expected);
}
#[test]
fn test_sanitize_uppercase_letters() {
let tool_name = ToolName::new("UPPERCASE_TOOL_NAME");
let actual = tool_name.into_sanitized();
let expected = ToolName::new("uppercase_tool_name");
assert_eq!(actual, expected);
}
#[test]
fn test_sanitize_numbers_only() {
let tool_name = ToolName::new("123456");
let actual = tool_name.into_sanitized();
let expected = ToolName::new("123456");
assert_eq!(actual, expected);
}
#[test]
fn test_sanitize_mixed_numbers_and_letters() {
let tool_name = ToolName::new("tool1Name2Test3");
let actual = tool_name.into_sanitized();
let expected = ToolName::new("tool1name2test3");
assert_eq!(actual, expected);
}
#[test]
fn test_sanitize_empty_string() {
let tool_name = ToolName::new("");
let actual = tool_name.into_sanitized();
let expected = ToolName::new("");
assert_eq!(actual, expected);
}
#[test]
fn test_sanitize_only_special_chars() {
let tool_name = ToolName::new("@#$%^&*()");
let actual = tool_name.into_sanitized();
let expected = ToolName::new("");
assert_eq!(actual, expected);
}
#[test]
fn test_sanitize_complex_mixed_case() {
let tool_name = ToolName::new("XMLHttpRequest2Handler");
let actual = tool_name.into_sanitized();
let expected = ToolName::new("xmlhttprequest2handler");
assert_eq!(actual, expected);
}
#[test]
fn test_sanitize_dots_and_slashes() {
let tool_name = ToolName::new("tool.name/with.dots");
let actual = tool_name.into_sanitized();
let expected = ToolName::new("tool_name_with_dots");
assert_eq!(actual, expected);
}
#[test]
fn test_sanitize_single_underscore_preserved() {
let tool_name = ToolName::new("tool_name");
let actual = tool_name.into_sanitized();
let expected = ToolName::new("tool_name");
assert_eq!(actual, expected);
}
#[test]
fn test_sanitize_camel_case_with_underscore() {
let tool_name = ToolName::new("camelCase_withUnderscore");
let actual = tool_name.into_sanitized();
let expected = ToolName::new("camelcase_withunderscore");
assert_eq!(actual, expected);
}
#[test]
fn test_sanitize_numbers_between_letters() {
let tool_name = ToolName::new("tool1tool2tool3");
let actual = tool_name.into_sanitized();
let expected = ToolName::new("tool1tool2tool3");
assert_eq!(actual, expected);
}
#[test]
fn test_sanitize_mixed_case_preserves_numbers() {
let tool_name = ToolName::new("Test123Case");
let actual = tool_name.into_sanitized();
let expected = ToolName::new("test123case");
assert_eq!(actual, expected);
}
}