CI / Build Pip package (push) Successful in 2m32s
When two routes share the same parameter at the same node position
(e.g. GET /restaurants/${id} and GET /restaurants/${id}/menu),
reuse the existing equivalent matcher instead of raising a conflict.
Non-equivalent matchers (different names, kinds, or glob patterns)
at the same node for the same method still raise ValueError.
159 lines
7.9 KiB
Python
159 lines
7.9 KiB
Python
from typing import Sequence, Tuple, Optional, List
|
|
|
|
from kaya.core import Tree, PathHandler, HttpContext, HttpMethod, PathIterator
|
|
from pwo import Maybe
|
|
import unittest
|
|
|
|
|
|
class PathIteratorTest(unittest.TestCase):
|
|
cases: Tuple[Tuple[str, Tuple[str, ...]], ...] = (
|
|
('/', tuple()),
|
|
('root/foo', ('root', 'foo')),
|
|
('/root', ('root',)),
|
|
('/root', ('root',)),
|
|
('/root/', ('root',)),
|
|
('/root/bar/', ('root', 'bar')),
|
|
)
|
|
|
|
def test_path_iterator(self):
|
|
for (case, expected) in self.cases:
|
|
with self.subTest(case) as _:
|
|
components = tuple((c for c in PathIterator(case)))
|
|
self.assertEqual(expected, components)
|
|
|
|
|
|
class TreeTest(unittest.TestCase):
|
|
tree: Tree
|
|
handlers: List[PathHandler]
|
|
|
|
def setUp(self):
|
|
self.tree = Tree()
|
|
|
|
class TestHandler(PathHandler):
|
|
|
|
def handle_request(self, ctx: HttpContext):
|
|
pass
|
|
|
|
@property
|
|
def recursive(self) -> bool:
|
|
return True
|
|
|
|
self.handlers = [TestHandler() for _ in range(20)]
|
|
|
|
routes: Tuple[Tuple[Tuple[str, ...], Optional[HttpMethod], PathHandler], ...] = (
|
|
(('home', 'something'), HttpMethod.GET, self.handlers[0]),
|
|
(('home', 'something_else'), HttpMethod.GET, self.handlers[1]),
|
|
(('home', 'something_else'), HttpMethod.POST, self.handlers[2]),
|
|
(('home', 'something', 'object'), HttpMethod.GET, self.handlers[3]),
|
|
(('home', 'something_else', 'foo'), HttpMethod.GET, self.handlers[4]),
|
|
(('home',), HttpMethod.GET, self.handlers[5]),
|
|
(('home',), HttpMethod.POST, self.handlers[6]),
|
|
(('home',), None, self.handlers[7]),
|
|
(('home', '*.md'), None, self.handlers[8]),
|
|
(('home', 'something', '*', 'blah', '*.md'), None, self.handlers[9]),
|
|
(('home', 'bar', '*'), None, self.handlers[10]),
|
|
|
|
)
|
|
|
|
for path, method, handler in routes:
|
|
self.tree.add((p for p in path), method, handler)
|
|
|
|
def test_tree(self):
|
|
|
|
cases: Tuple[Tuple[str, HttpMethod, Optional[int]], ...] = (
|
|
('http://localhost:127.0.0.1:5432/home/something', HttpMethod.GET, 0),
|
|
('http://localhost:127.0.0.1:5432/home/something_else', HttpMethod.GET, 1),
|
|
('http://localhost:127.0.0.1:5432/home/something_else', HttpMethod.POST, 2),
|
|
('http://localhost:127.0.0.1:5432/home/something/object', HttpMethod.GET, 3),
|
|
('http://localhost:127.0.0.1:5432/home/something_else/foo', HttpMethod.GET, 4),
|
|
('http://localhost:127.0.0.1:5432/', HttpMethod.GET, None),
|
|
('http://localhost:127.0.0.1:5432/home', HttpMethod.GET, 5),
|
|
('http://localhost:127.0.0.1:5432/home', HttpMethod.POST, 6),
|
|
('http://localhost:127.0.0.1:5432/home', HttpMethod.PUT, 7),
|
|
('http://localhost:127.0.0.1:5432/home/README.md', HttpMethod.GET, 8),
|
|
('http://localhost:127.0.0.1:5432/home/something/ciao/blah/README.md', HttpMethod.GET, 9),
|
|
('http://localhost:127.0.0.1:5432/home/bar/ciao/blah/README.md', HttpMethod.GET, 10),
|
|
)
|
|
for url, method, handler_num in cases:
|
|
with self.subTest(f"{str(method)} {url}"):
|
|
res = self.tree.get_handler(url, method)
|
|
self.assertIs(Maybe.of(handler_num).map(self.handlers.__getitem__).or_none(),
|
|
Maybe.of_nullable(res).map(lambda it: it[0]).or_none())
|
|
|
|
def test_two_method_agnostic_matchers_raise(self):
|
|
tree = Tree()
|
|
tree.add((p for p in ('foo', '*')), None, self.handlers[0])
|
|
with self.assertRaises(ValueError):
|
|
tree.add((p for p in ('foo', '*.md')), None, self.handlers[1])
|
|
|
|
def test_identical_method_agnostic_matchers_reuse(self):
|
|
tree = Tree()
|
|
tree.add((p for p in ('foo', '*')), None, self.handlers[0])
|
|
tree.add((p for p in ('foo', '*')), None, self.handlers[1])
|
|
handler = Maybe.of_nullable(tree.get_handler('/foo/bar', HttpMethod.GET)).map(lambda it: it[0]).or_none()
|
|
self.assertIs(self.handlers[0], handler)
|
|
|
|
def test_two_overlapping_method_specific_matchers_raise(self):
|
|
tree = Tree()
|
|
tree.add((p for p in ('foo', '${id:int}')), HttpMethod.PUT, self.handlers[0])
|
|
with self.assertRaises(ValueError):
|
|
tree.add((p for p in ('foo', '${name:str}')), HttpMethod.PUT, self.handlers[1])
|
|
|
|
def test_disjoint_method_specific_matchers_allowed(self):
|
|
tree = Tree()
|
|
tree.add((p for p in ('foo', '*')), HttpMethod.PUT, self.handlers[0])
|
|
tree.add((p for p in ('foo', '*')), HttpMethod.GET, self.handlers[1])
|
|
put_handler = Maybe.of_nullable(tree.get_handler('/foo/bar', HttpMethod.PUT)).map(lambda it: it[0]).or_none()
|
|
get_handler = Maybe.of_nullable(tree.get_handler('/foo/bar', HttpMethod.GET)).map(lambda it: it[0]).or_none()
|
|
self.assertIs(self.handlers[0], put_handler)
|
|
self.assertIs(self.handlers[1], get_handler)
|
|
|
|
def test_nested_routes_with_same_param_allowed(self):
|
|
tree = Tree()
|
|
tree.add((p for p in ('restaurants', '${id}')), HttpMethod.GET, self.handlers[0])
|
|
tree.add((p for p in ('restaurants', '${id}', 'menu')), HttpMethod.GET, self.handlers[1])
|
|
h0 = Maybe.of_nullable(tree.get_handler('/restaurants/42', HttpMethod.GET)).map(lambda it: it[0]).or_none()
|
|
h1 = Maybe.of_nullable(tree.get_handler('/restaurants/42/menu', HttpMethod.GET)).map(lambda it: it[0]).or_none()
|
|
self.assertIs(self.handlers[0], h0)
|
|
self.assertIs(self.handlers[1], h1)
|
|
|
|
def test_nested_routes_same_param_reverse_order(self):
|
|
tree = Tree()
|
|
tree.add((p for p in ('restaurants', '${id}', 'menu')), HttpMethod.GET, self.handlers[1])
|
|
tree.add((p for p in ('restaurants', '${id}')), HttpMethod.GET, self.handlers[0])
|
|
h0 = Maybe.of_nullable(tree.get_handler('/restaurants/42', HttpMethod.GET)).map(lambda it: it[0]).or_none()
|
|
h1 = Maybe.of_nullable(tree.get_handler('/restaurants/42/menu', HttpMethod.GET)).map(lambda it: it[0]).or_none()
|
|
self.assertIs(self.handlers[0], h0)
|
|
self.assertIs(self.handlers[1], h1)
|
|
|
|
def test_nested_routes_with_same_int_param_allowed(self):
|
|
tree = Tree()
|
|
tree.add((p for p in ('restaurants', '${id:int}')), HttpMethod.GET, self.handlers[0])
|
|
tree.add((p for p in ('restaurants', '${id:int}', 'menu')), HttpMethod.GET, self.handlers[1])
|
|
h0 = Maybe.of_nullable(tree.get_handler('/restaurants/42', HttpMethod.GET)).map(lambda it: it[0]).or_none()
|
|
h1 = Maybe.of_nullable(tree.get_handler('/restaurants/42/menu', HttpMethod.GET)).map(lambda it: it[0]).or_none()
|
|
self.assertIs(self.handlers[0], h0)
|
|
self.assertIs(self.handlers[1], h1)
|
|
|
|
def test_nested_routes_method_agnostic_reuses_matcher(self):
|
|
tree = Tree()
|
|
tree.add((p for p in ('restaurants', '${id}')), HttpMethod.GET, self.handlers[0])
|
|
tree.add((p for p in ('restaurants', '${id}', 'menu')), None, self.handlers[1])
|
|
h0 = Maybe.of_nullable(tree.get_handler('/restaurants/42', HttpMethod.GET)).map(lambda it: it[0]).or_none()
|
|
h1 = Maybe.of_nullable(tree.get_handler('/restaurants/42/menu', HttpMethod.POST)).map(lambda it: it[0]).or_none()
|
|
self.assertIs(self.handlers[0], h0)
|
|
self.assertIs(self.handlers[1], h1)
|
|
|
|
def test_different_param_names_still_raise(self):
|
|
tree = Tree()
|
|
tree.add((p for p in ('a', '${id}')), HttpMethod.GET, self.handlers[0])
|
|
with self.assertRaises(ValueError):
|
|
tree.add((p for p in ('a', '${name}', 'x')), HttpMethod.GET, self.handlers[1])
|
|
|
|
def test_different_param_kinds_still_raise(self):
|
|
tree = Tree()
|
|
tree.add((p for p in ('a', '${id}')), HttpMethod.GET, self.handlers[0])
|
|
with self.assertRaises(ValueError):
|
|
tree.add((p for p in ('a', '${id:int}', 'x')), HttpMethod.GET, self.handlers[1])
|
|
|