File size: 7,961 Bytes
bcc8074
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
"""
Route Matcher - URL pattern matching for service configuration.

Provides flexible URL matching capabilities for services to define
which routes require auth, credits, etc.

Supported patterns:
- Exact match: "/api/users"
- Prefix match: "/api/*" (matches /api/anything)
- Wildcard match: "/api/users/*/posts" (matches /api/users/123/posts)
- Deep wildcard: "/api/**" (matches /api/users/123/posts/456)
- Regex match: "^/api/v[0-9]+/.*$"

Usage:
    matcher = RouteMatcher(["/api/*", "/admin/**"])
    
    if matcher.matches("/api/users"):
        # Route requires auth
        pass
"""

import re
import logging
from typing import List, Set, Optional, Pattern
from fnmatch import fnmatch

logger = logging.getLogger(__name__)


class RouteMatcher:
    """
    Flexible URL pattern matcher for route configuration.
    
    Supports exact matches, glob patterns, and regex patterns.
    """
    
    def __init__(self, patterns: List[str]):
        """
        Initialize route matcher with patterns.
        
        Args:
            patterns: List of URL patterns to match
        """
        self.patterns = patterns
        self._exact_matches: Set[str] = set()
        self._prefix_patterns: List[str] = []
        self._glob_patterns: List[str] = []
        self._regex_patterns: List[Pattern] = []
        
        # Classify patterns for performance
        self._classify_patterns()
    
    def _classify_patterns(self) -> None:
        """
        Classify patterns by type for optimal matching performance.
        
        Order of matching:
        1. Exact matches (fastest - O(1))
        2. Prefix patterns (fast - string startswith)
        3. Glob patterns (medium - fnmatch)
        4. Regex patterns (slowest - regex matching)
        """
        for pattern in self.patterns:
            # Empty pattern
            if not pattern:
                continue
            
            # Regex pattern (starts with ^)
            if pattern.startswith("^"):
                try:
                    compiled = re.compile(pattern)
                    self._regex_patterns.append(compiled)
                    logger.debug(f"Classified as regex: {pattern}")
                except re.error as e:
                    logger.warning(f"Invalid regex pattern '{pattern}': {e}")
                continue
            
            # Glob pattern (contains * or ?)
            if "*" in pattern or "?" in pattern:
                # Simple prefix wildcard: /api/*
                if pattern.endswith("/*") and "*" not in pattern[:-2]:
                    prefix = pattern[:-2]  # Remove /*
                    self._prefix_patterns.append(prefix)
                    logger.debug(f"Classified as prefix: {prefix}")
                else:
                    # Complex glob: /api/*/users or /api/**
                    self._glob_patterns.append(pattern)
                    logger.debug(f"Classified as glob: {pattern}")
                continue
            
            # Exact match
            self._exact_matches.add(pattern)
            logger.debug(f"Classified as exact: {pattern}")
    
    def matches(self, path: str) -> bool:
        """
        Check if a URL path matches any configured pattern.
        
        Args:
            path: URL path to check (e.g., "/api/users/123")
        
        Returns:
            True if path matches any pattern, False otherwise
        """
        # Strip query parameters and fragments
        path = path.split("?")[0].split("#")[0]
        
        # Normalize path (remove trailing slash unless it's just "/")
        if path != "/" and path.endswith("/"):
            path = path.rstrip("/")
        
        # 1. Exact match (O(1))
        if path in self._exact_matches:
            return True
        
        # 2. Prefix match (O(n) but fast)
        for prefix in self._prefix_patterns:
            if path.startswith(prefix + "/") or path == prefix:
                return True
        
        # 3. Glob match (O(n))
        for pattern in self._glob_patterns:
            if fnmatch(path, pattern):
                return True
        
        # 4. Regex match (O(n) but slower)
        for regex in self._regex_patterns:
            if regex.match(path):
                return True
        
        return False
    
    def get_matching_pattern(self, path: str) -> Optional[str]:
        """
        Get the first pattern that matches the given path.
        
        Useful for debugging or determining which rule matched.
        
        Args:
            path: URL path to check
        
        Returns:
            Matching pattern string or None
        """
        # Strip query parameters and fragments
        path = path.split("?")[0].split("#")[0]
        
        # Normalize path
        if path != "/" and path.endswith("/"):
            path = path.rstrip("/")
        
        # Exact match
        if path in self._exact_matches:
            return path
        
        # Prefix match
        for prefix in self._prefix_patterns:
            if path.startswith(prefix + "/") or path == prefix:
                return prefix + "/*"
        
        # Glob match
        for pattern in self._glob_patterns:
            if fnmatch(path, pattern):
                return pattern
        
        # Regex match
        for regex in self._regex_patterns:
            if regex.match(path):
                return regex.pattern
        
        return None
    
    def __repr__(self) -> str:
        """String representation for debugging."""
        return (
            f"RouteMatcher("
            f"exact={len(self._exact_matches)}, "
            f"prefix={len(self._prefix_patterns)}, "
            f"glob={len(self._glob_patterns)}, "
            f"regex={len(self._regex_patterns)})"
        )


class RouteConfig:
    """
    Route configuration helper for services.
    
    Manages multiple route lists (required, optional, public) with
    precedence and exclusion logic.
    """
    
    def __init__(
        self,
        required: List[str] = None,
        optional: List[str] = None,
        public: List[str] = None,
    ):
        """
        Initialize route configuration.
        
        Args:
            required: Routes that REQUIRE the service (e.g., auth required)
            optional: Routes where service is OPTIONAL (e.g., auth optional)
            public: Routes that are PUBLIC (e.g., no auth needed)
        
        Precedence: public > required > optional (for conflict resolution)
        """
        self.required_matcher = RouteMatcher(required or [])
        self.optional_matcher = RouteMatcher(optional or [])
        self.public_matcher = RouteMatcher(public or [])
    
    def is_required(self, path: str) -> bool:
        """
        Check if service is REQUIRED for this path.
        
        Returns False if path is public (public takes precedence).
        """
        if self.is_public(path):
            return False
        return self.required_matcher.matches(path)
    
    def is_optional(self, path: str) -> bool:
        """
        Check if service is OPTIONAL for this path.
        
        Returns False if path is public or required.
        """
        if self.is_public(path):
            return False
        if self.required_matcher.matches(path):
            return False
        return self.optional_matcher.matches(path)
    
    def is_public(self, path: str) -> bool:
        """
        Check if path is PUBLIC (service not needed).
        
        Public takes highest precedence.
        """
        return self.public_matcher.matches(path)
    
    def requires_service(self, path: str) -> bool:
        """
        Check if service is needed (required OR optional) for this path.
        
        Returns False if path is not matched by any configuration.
        """
        return self.is_required(path) or self.is_optional(path)


__all__ = [
    'RouteMatcher',
    'RouteConfig',
]