8
8
"""
9
9
10
10
try :
11
- from typing import Callable , List , Iterable , Union , Tuple , Dict , TYPE_CHECKING
11
+ from typing import Callable , Iterable , Union , Tuple , Literal , Dict , TYPE_CHECKING
12
12
13
13
if TYPE_CHECKING :
14
14
from .response import Response
23
23
class Route :
24
24
"""Route definition for different paths, see `adafruit_httpserver.server.Server.route`."""
25
25
26
+ @staticmethod
27
+ def _prepare_path_pattern (path : str , append_slash : bool ) -> str :
28
+ # Escape all dots
29
+ path = re .sub (r"\." , r"\\." , path )
30
+
31
+ # Replace url parameters with regex groups
32
+ path = re .sub (r"<\w+>" , r"([^/]+)" , path )
33
+
34
+ # Replace wildcards with corresponding regex
35
+ path = path .replace (r"\.\.\.\." , r".+" ).replace (r"\.\.\." , r"[^/]+" )
36
+
37
+ # Add optional slash at the end if append_slash is True
38
+ if append_slash :
39
+ path += r"/?"
40
+
41
+ # Add start and end of string anchors
42
+ return f"^{ path } $"
43
+
26
44
def __init__ (
27
45
self ,
28
46
path : str = "" ,
@@ -33,80 +51,89 @@ def __init__(
33
51
) -> None :
34
52
self ._validate_path (path , append_slash )
35
53
36
- self .parameters_names = [
37
- name [1 :- 1 ] for name in re .compile (r"/[^<>]*/?" ).split (path ) if name != ""
38
- ]
39
- self .path = re .sub (r"<\w+>" , r"([^/]+)" , path ).replace ("...." , r".+" ).replace (
40
- "..." , r"[^/]+"
41
- ) + ("/?" if append_slash else "" )
54
+ self .path = path
42
55
self .methods = (
43
56
set (methods ) if isinstance (methods , (set , list , tuple )) else set ([methods ])
44
57
)
45
-
46
58
self .handler = handler
59
+ self .parameters_names = [
60
+ name [1 :- 1 ] for name in re .compile (r"/[^<>]*/?" ).split (path ) if name != ""
61
+ ]
62
+ self .path_pattern = re .compile (self ._prepare_path_pattern (path , append_slash ))
47
63
48
64
@staticmethod
49
65
def _validate_path (path : str , append_slash : bool ) -> None :
50
66
if not path .startswith ("/" ):
51
67
raise ValueError ("Path must start with a slash." )
52
68
69
+ if path .endswith ("/" ) and append_slash :
70
+ raise ValueError ("Cannot use append_slash=True when path ends with /" )
71
+
72
+ if "//" in path :
73
+ raise ValueError ("Path cannot contain double slashes." )
74
+
53
75
if "<>" in path :
54
76
raise ValueError ("All URL parameters must be named." )
55
77
56
- if path .endswith ("/" ) and append_slash :
57
- raise ValueError ("Cannot use append_slash=True when path ends with /" )
78
+ if re .search (r"[^/]<[^/]+>|<[^/]+>[^/]" , path ):
79
+ raise ValueError ("All URL parameters must be between slashes." )
80
+
81
+ if re .search (r"[^/.]\.\.\.\.?|\.?\.\.\.[^/.]" , path ):
82
+ raise ValueError ("... and .... must be between slashes" )
58
83
59
- def match (self , other : "Route" ) -> Tuple [bool , Dict [str , str ]]:
84
+ if "....." in path :
85
+ raise ValueError ("Path cannot contain more than 4 dots in a row." )
86
+
87
+ def matches (
88
+ self , method : str , path : str
89
+ ) -> Union [Tuple [Literal [False ], None ], Tuple [Literal [True ], Dict [str , str ]]]:
60
90
"""
61
- Checks if the route matches the other route .
91
+ Checks if the route matches given ``method`` and ``path`` .
62
92
63
- If the route contains parameters, it will check if the ``other`` route contains values for
93
+ If the route contains parameters, it will check if the ``path`` contains values for
64
94
them.
65
95
66
- Returns tuple of a boolean and a list of strings. The boolean indicates if the routes match,
67
- and the list contains the values of the url parameters from the ``other`` route.
96
+ Returns tuple of a boolean that indicates if the routes matches and a dict containing
97
+ values for url parameters.
98
+ If the route does not match ``path`` or ``method`` if will return ``None`` instead of dict.
68
99
69
100
Examples::
70
101
71
- route = Route("/example", GET, True)
102
+ route = Route("/example", GET, append_slash= True)
72
103
73
- other1a = Route("/example", GET)
74
- other1b = Route("/example/", GET)
75
- route.matches(other1a) # True, {}
76
- route.matches(other1b) # True, {}
104
+ route.matches(GET, "/example") # True, {}
105
+ route.matches(GET, "/example/") # True, {}
77
106
78
- other2 = Route( "/other-example", GET)
79
- route.matches(other2 ) # False, {}
107
+ route.matches(GET, "/other-example") # False, None
108
+ route.matches(POST, "/example/" ) # False, None
80
109
81
110
...
82
111
83
112
route = Route("/example/<parameter>", GET)
84
113
85
- other1 = Route("/example/123", GET)
86
- route.matches(other1) # True, {"parameter": "123"}
114
+ route.matches(GET, "/example/123") # True, {"parameter": "123"}
87
115
88
- other2 = Route("/other-example", GET)
89
- route.matches(other2) # False, {}
116
+ route.matches(GET, "/other-example") # False, None
90
117
91
118
...
92
119
93
- route1 = Route("/example/.../something", GET)
94
- other1 = Route("/example/123/something", GET)
95
- route1.matches(other1) # True, {}
120
+ route = Route("/example/.../something", GET)
121
+ route.matches(GET, "/example/123/something") # True, {}
96
122
97
- route2 = Route("/example/..../something", GET)
98
- other2 = Route("/example/123/456/something", GET)
99
- route2.matches(other2) # True, {}
123
+ route = Route("/example/..../something", GET)
124
+ route.matches(GET, "/example/123/456/something") # True, {}
100
125
"""
101
126
102
- if not other .methods .issubset (self .methods ):
103
- return False , {}
127
+ if method not in self .methods :
128
+ return False , None
129
+
130
+ path_match = self .path_pattern .match (path )
131
+ if path_match is None :
132
+ return False , None
104
133
105
- regex_match = re .match (f"^{ self .path } $" , other .path )
106
- if regex_match is None :
107
- return False , {}
134
+ url_parameters_values = path_match .groups ()
108
135
109
- return True , dict (zip (self .parameters_names , regex_match . groups () ))
136
+ return True , dict (zip (self .parameters_names , url_parameters_values ))
110
137
111
138
def __repr__ (self ) -> str :
112
139
path = repr (self .path )
@@ -168,51 +195,3 @@ def route_decorator(func: Callable) -> Route:
168
195
return Route (path , methods , func , append_slash = append_slash )
169
196
170
197
return route_decorator
171
-
172
-
173
- class _Routes :
174
- """A collection of routes and their corresponding handlers."""
175
-
176
- def __init__ (self ) -> None :
177
- self ._routes : List [Route ] = []
178
-
179
- def add (self , route : Route ):
180
- """Adds a route and its handler to the collection."""
181
- self ._routes .append (route )
182
-
183
- def find_handler (self , route : Route ) -> Union [Callable ["..." , "Response" ], None ]:
184
- """
185
- Finds a handler for a given route.
186
-
187
- If route used URL parameters, the handler will be wrapped to pass the parameters to the
188
- handler.
189
-
190
- Example::
191
-
192
- @server.route("/example/<my_parameter>", GET)
193
- def route_func(request, my_parameter):
194
- ...
195
- request.path == "/example/123" # True
196
- my_parameter == "123" # True
197
- """
198
- found_route , _route = False , None
199
-
200
- for _route in self ._routes :
201
- matches , keyword_parameters = _route .match (route )
202
-
203
- if matches :
204
- found_route = True
205
- break
206
-
207
- if not found_route :
208
- return None
209
-
210
- handler = _route .handler
211
-
212
- def wrapped_handler (request ):
213
- return handler (request , ** keyword_parameters )
214
-
215
- return wrapped_handler
216
-
217
- def __repr__ (self ) -> str :
218
- return f"_Routes({ repr (self ._routes )} )"
0 commit comments