Skip to main content

next_rs_middleware/
matcher.rs

1use regex::Regex;
2
3#[derive(Debug, Clone)]
4pub enum PathMatcher {
5    Exact(String),
6    Prefix(String),
7    Regex(String),
8    All,
9}
10
11impl PathMatcher {
12    pub fn matches(&self, path: &str) -> bool {
13        match self {
14            PathMatcher::Exact(p) => path == p,
15            PathMatcher::Prefix(p) => path.starts_with(p),
16            PathMatcher::Regex(pattern) => Regex::new(pattern)
17                .map(|re| re.is_match(path))
18                .unwrap_or(false),
19            PathMatcher::All => true,
20        }
21    }
22}
23
24#[derive(Debug, Clone)]
25pub struct MiddlewareMatcher {
26    include: Vec<PathMatcher>,
27    exclude: Vec<PathMatcher>,
28}
29
30impl MiddlewareMatcher {
31    pub fn new() -> Self {
32        Self {
33            include: Vec::new(),
34            exclude: Vec::new(),
35        }
36    }
37
38    pub fn include(mut self, matcher: PathMatcher) -> Self {
39        self.include.push(matcher);
40        self
41    }
42
43    pub fn exclude(mut self, matcher: PathMatcher) -> Self {
44        self.exclude.push(matcher);
45        self
46    }
47
48    pub fn matches(&self, path: &str) -> bool {
49        for excluded in &self.exclude {
50            if excluded.matches(path) {
51                return false;
52            }
53        }
54
55        if self.include.is_empty() {
56            return true;
57        }
58
59        for included in &self.include {
60            if included.matches(path) {
61                return true;
62            }
63        }
64
65        false
66    }
67
68    pub fn from_config(patterns: Vec<&str>) -> Self {
69        let mut matcher = Self::new();
70        for pattern in patterns {
71            if pattern.ends_with("*") {
72                matcher.include.push(PathMatcher::Prefix(
73                    pattern.trim_end_matches('*').to_string(),
74                ));
75            } else if pattern.starts_with("^") || pattern.contains("(") {
76                matcher
77                    .include
78                    .push(PathMatcher::Regex(pattern.to_string()));
79            } else {
80                matcher
81                    .include
82                    .push(PathMatcher::Exact(pattern.to_string()));
83            }
84        }
85        matcher
86    }
87}
88
89impl Default for MiddlewareMatcher {
90    fn default() -> Self {
91        Self::new()
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn test_exact_matcher() {
101        let matcher = PathMatcher::Exact("/about".to_string());
102        assert!(matcher.matches("/about"));
103        assert!(!matcher.matches("/about/"));
104        assert!(!matcher.matches("/about/team"));
105    }
106
107    #[test]
108    fn test_prefix_matcher() {
109        let matcher = PathMatcher::Prefix("/api/".to_string());
110        assert!(matcher.matches("/api/users"));
111        assert!(matcher.matches("/api/posts/1"));
112        assert!(!matcher.matches("/about"));
113    }
114
115    #[test]
116    fn test_regex_matcher() {
117        let matcher = PathMatcher::Regex(r"^/blog/\d+$".to_string());
118        assert!(matcher.matches("/blog/123"));
119        assert!(!matcher.matches("/blog/abc"));
120    }
121
122    #[test]
123    fn test_middleware_matcher_include() {
124        let matcher = MiddlewareMatcher::new()
125            .include(PathMatcher::Prefix("/api/".to_string()))
126            .include(PathMatcher::Prefix("/admin/".to_string()));
127
128        assert!(matcher.matches("/api/users"));
129        assert!(matcher.matches("/admin/dashboard"));
130        assert!(!matcher.matches("/public/file"));
131    }
132
133    #[test]
134    fn test_middleware_matcher_exclude() {
135        let matcher = MiddlewareMatcher::new()
136            .include(PathMatcher::All)
137            .exclude(PathMatcher::Prefix("/static/".to_string()))
138            .exclude(PathMatcher::Prefix("/_next/".to_string()));
139
140        assert!(matcher.matches("/api/users"));
141        assert!(!matcher.matches("/static/image.png"));
142        assert!(!matcher.matches("/_next/chunk.js"));
143    }
144
145    #[test]
146    fn test_from_config() {
147        let matcher = MiddlewareMatcher::from_config(vec!["/api/*", "/admin/*", "/login"]);
148
149        assert!(matcher.matches("/api/users"));
150        assert!(matcher.matches("/admin/"));
151        assert!(matcher.matches("/login"));
152        assert!(!matcher.matches("/public"));
153    }
154}