use regex::Regex;
use crate::error::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum McpType {
#[default]
Tool,
Resource,
Skip,
}
#[derive(Debug, Clone)]
pub struct RouteRule {
pub methods: Vec<String>,
pub pattern: Option<Regex>,
pub mcp_type: McpType,
pub priority: i32,
}
impl RouteRule {
pub fn new(mcp_type: McpType) -> Self {
Self {
methods: Vec::new(),
pattern: None,
mcp_type,
priority: 0,
}
}
#[must_use]
pub fn methods<I, S>(mut self, methods: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.methods = methods.into_iter().map(Into::into).collect();
self
}
pub fn pattern(mut self, pattern: &str) -> Result<Self> {
self.pattern = Some(Regex::new(pattern)?);
Ok(self)
}
#[must_use]
pub fn priority(mut self, priority: i32) -> Self {
self.priority = priority;
self
}
pub fn matches(&self, method: &str, path: &str) -> bool {
if !self.methods.is_empty() && !self.methods.iter().any(|m| m.eq_ignore_ascii_case(method))
{
return false;
}
if let Some(ref pattern) = self.pattern
&& !pattern.is_match(path)
{
return false;
}
true
}
}
#[derive(Debug, Clone, Default)]
pub struct RouteMapping {
rules: Vec<RouteRule>,
}
impl RouteMapping {
pub fn new() -> Self {
Self::default()
}
pub fn default_rules() -> Self {
Self::new()
.map_methods(["GET"], McpType::Resource)
.map_methods(["POST", "PUT", "PATCH", "DELETE"], McpType::Tool)
}
#[must_use]
pub fn map_methods<I, S>(mut self, methods: I, mcp_type: McpType) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.rules.push(RouteRule::new(mcp_type).methods(methods));
self
}
#[must_use]
pub fn map_method(self, method: &str, mcp_type: McpType) -> Self {
self.map_methods([method], mcp_type)
}
pub fn map_pattern(mut self, pattern: &str, mcp_type: McpType) -> Result<Self> {
self.rules.push(RouteRule::new(mcp_type).pattern(pattern)?);
Ok(self)
}
pub fn map_rule<I, S>(
mut self,
methods: I,
pattern: &str,
mcp_type: McpType,
priority: i32,
) -> Result<Self>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.rules.push(
RouteRule::new(mcp_type)
.methods(methods)
.pattern(pattern)?
.priority(priority),
);
Ok(self)
}
#[must_use]
pub fn add_rule(mut self, rule: RouteRule) -> Self {
self.rules.push(rule);
self
}
pub fn skip_pattern(self, pattern: &str) -> Result<Self> {
self.map_pattern(pattern, McpType::Skip)
}
pub fn get_mcp_type(&self, method: &str, path: &str) -> McpType {
let mut sorted_rules: Vec<_> = self.rules.iter().collect();
sorted_rules.sort_by(|a, b| b.priority.cmp(&a.priority));
for rule in sorted_rules {
if rule.matches(method, path) {
return rule.mcp_type;
}
}
match method.to_uppercase().as_str() {
"GET" => McpType::Resource,
_ => McpType::Tool,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_rules() {
let mapping = RouteMapping::default_rules();
assert_eq!(mapping.get_mcp_type("GET", "/users"), McpType::Resource);
assert_eq!(mapping.get_mcp_type("POST", "/users"), McpType::Tool);
assert_eq!(mapping.get_mcp_type("PUT", "/users/1"), McpType::Tool);
assert_eq!(mapping.get_mcp_type("DELETE", "/users/1"), McpType::Tool);
}
#[test]
fn test_pattern_matching() {
let mapping = RouteMapping::new()
.map_pattern(r"/admin/.*", McpType::Skip)
.unwrap()
.map_methods(["GET"], McpType::Resource);
assert_eq!(mapping.get_mcp_type("GET", "/admin/users"), McpType::Skip);
assert_eq!(mapping.get_mcp_type("GET", "/users"), McpType::Resource);
}
#[test]
fn test_priority() {
let mapping = RouteMapping::new()
.add_rule(
RouteRule::new(McpType::Resource)
.methods(["GET"])
.priority(0),
)
.add_rule(
RouteRule::new(McpType::Tool)
.pattern(r"/api/.*")
.unwrap()
.priority(10),
);
assert_eq!(mapping.get_mcp_type("GET", "/api/users"), McpType::Tool);
assert_eq!(mapping.get_mcp_type("GET", "/users"), McpType::Resource);
}
#[test]
fn test_route_rule_matches() {
let rule = RouteRule::new(McpType::Tool)
.methods(["POST", "PUT"])
.pattern(r"/users/\d+")
.unwrap();
assert!(rule.matches("POST", "/users/123"));
assert!(rule.matches("PUT", "/users/456"));
assert!(!rule.matches("GET", "/users/123")); assert!(!rule.matches("POST", "/users/abc")); }
}