aws_lambda_router/
matcher.rs

1use regex::Regex;
2use lazy_static::lazy_static;
3use std::collections::HashMap;
4
5lazy_static! {
6    static ref PARAM_REGEX: Regex = Regex::new(r":([a-zA-Z_][a-zA-Z0-9_]*)").unwrap();
7}
8
9/// Path matcher with parameter extraction
10#[derive(Debug, Clone)]
11pub struct PathMatcher {
12    pattern: String,
13    regex: Regex,
14    param_names: Vec<String>,
15}
16
17impl PathMatcher {
18    /// Create a new PathMatcher from a route pattern
19    /// Supports Express-like patterns: /api/users/:userId/posts/:postId
20    pub fn new(pattern: &str) -> Self {
21        let mut param_names = Vec::new();
22        
23        // Extract parameter names
24        for cap in PARAM_REGEX.captures_iter(pattern) {
25            param_names.push(cap[1].to_string());
26        }
27        
28        // Convert Express-style pattern to regex
29        let regex_pattern = PARAM_REGEX
30            .replace_all(pattern, r"([^/]+)")
31            .replace("/", r"/");
32        
33        let regex_pattern = format!("^{}$", regex_pattern);
34        let regex = Regex::new(&regex_pattern).unwrap();
35        
36        Self {
37            pattern: pattern.to_string(),
38            regex,
39            param_names,
40        }
41    }
42    
43    /// Check if path matches this pattern and extract parameters
44    pub fn matches(&self, path: &str) -> Option<HashMap<String, String>> {
45        self.regex.captures(path).map(|captures| {
46            self.param_names
47                .iter()
48                .enumerate()
49                .filter_map(|(i, name)| {
50                    captures.get(i + 1).map(|m| (name.clone(), m.as_str().to_string()))
51                })
52                .collect()
53        })
54    }
55    
56    /// Get the original pattern
57    pub fn pattern(&self) -> &str {
58        &self.pattern
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn test_simple_path() {
68        let matcher = PathMatcher::new("/api/users");
69        assert!(matcher.matches("/api/users").is_some());
70        assert!(matcher.matches("/api/posts").is_none());
71    }
72
73    #[test]
74    fn test_single_param() {
75        let matcher = PathMatcher::new("/api/users/:userId");
76        
77        let params = matcher.matches("/api/users/123").unwrap();
78        assert_eq!(params.get("userId"), Some(&"123".to_string()));
79        
80        assert!(matcher.matches("/api/users").is_none());
81        assert!(matcher.matches("/api/users/123/extra").is_none());
82    }
83
84    #[test]
85    fn test_multiple_params() {
86        let matcher = PathMatcher::new("/api/users/:userId/posts/:postId");
87        
88        let params = matcher.matches("/api/users/123/posts/456").unwrap();
89        assert_eq!(params.get("userId"), Some(&"123".to_string()));
90        assert_eq!(params.get("postId"), Some(&"456".to_string()));
91    }
92
93    #[test]
94    fn test_nested_paths() {
95        let matcher = PathMatcher::new("/api/chatbots/:chatbotId/conversations/:conversationId");
96        
97        let params = matcher.matches("/api/chatbots/chatbot123/conversations/conv456").unwrap();
98        assert_eq!(params.get("chatbotId"), Some(&"chatbot123".to_string()));
99        assert_eq!(params.get("conversationId"), Some(&"conv456".to_string()));
100    }
101}