next_rs_middleware/
matcher.rs1use 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}