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]) def test_nested_routes_different_methods_share_subtree(self): tree = Tree() tree.add((p for p in ('restaurants', '${id}', 'menu')), HttpMethod.GET, self.handlers[0]) tree.add((p for p in ('restaurants', '${id}', 'menu')), HttpMethod.POST, self.handlers[1]) h0 = Maybe.of_nullable(tree.get_handler('/restaurants/42/menu', 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_nested_routes_different_methods_reverse_order(self): tree = Tree() tree.add((p for p in ('restaurants', '${id}', 'menu')), HttpMethod.POST, self.handlers[1]) tree.add((p for p in ('restaurants', '${id}', 'menu')), HttpMethod.GET, self.handlers[0]) h0 = Maybe.of_nullable(tree.get_handler('/restaurants/42/menu', 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_nested_routes_combined_detail_and_menu_methods(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]) tree.add((p for p in ('restaurants', '${id}', 'menu')), HttpMethod.POST, self.handlers[2]) 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() h2 = 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) self.assertIs(self.handlers[2], h2)