88"""
99
1010try :
11- from typing import Callable , List , Iterable , Union , Tuple , Dict , TYPE_CHECKING
11+ from typing import Callable , Iterable , Union , Tuple , Literal , Dict , TYPE_CHECKING
1212
1313 if TYPE_CHECKING :
1414 from .response import Response
2323class Route :
2424 """Route definition for different paths, see `adafruit_httpserver.server.Server.route`."""
2525
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+
2644 def __init__ (
2745 self ,
2846 path : str = "" ,
@@ -33,80 +51,89 @@ def __init__(
3351 ) -> None :
3452 self ._validate_path (path , append_slash )
3553
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
4255 self .methods = (
4356 set (methods ) if isinstance (methods , (set , list , tuple )) else set ([methods ])
4457 )
45-
4658 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 ))
4763
4864 @staticmethod
4965 def _validate_path (path : str , append_slash : bool ) -> None :
5066 if not path .startswith ("/" ):
5167 raise ValueError ("Path must start with a slash." )
5268
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+
5375 if "<>" in path :
5476 raise ValueError ("All URL parameters must be named." )
5577
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" )
5883
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 ]]]:
6090 """
61- Checks if the route matches the other route .
91+ Checks if the route matches given ``method`` and ``path`` .
6292
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
6494 them.
6595
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.
6899
69100 Examples::
70101
71- route = Route("/example", GET, True)
102+ route = Route("/example", GET, append_slash= True)
72103
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, {}
77106
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
80109
81110 ...
82111
83112 route = Route("/example/<parameter>", GET)
84113
85- other1 = Route("/example/123", GET)
86- route.matches(other1) # True, {"parameter": "123"}
114+ route.matches(GET, "/example/123") # True, {"parameter": "123"}
87115
88- other2 = Route("/other-example", GET)
89- route.matches(other2) # False, {}
116+ route.matches(GET, "/other-example") # False, None
90117
91118 ...
92119
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, {}
96122
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, {}
100125 """
101126
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
104133
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 ()
108135
109- return True , dict (zip (self .parameters_names , regex_match . groups () ))
136+ return True , dict (zip (self .parameters_names , url_parameters_values ))
110137
111138 def __repr__ (self ) -> str :
112139 path = repr (self .path )
@@ -168,51 +195,3 @@ def route_decorator(func: Callable) -> Route:
168195 return Route (path , methods , func , append_slash = append_slash )
169196
170197 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