Skip to content

Commit 4d97c68

Browse files
committed
Added test for nested controller and module
1 parent 326372b commit 4d97c68

File tree

8 files changed

+243
-10
lines changed

8 files changed

+243
-10
lines changed

ellar/common/models/controller.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ def full_view_name(cls, name: str) -> str:
2929
return f"{cls.controller_class_name()}/{name}"
3030

3131
def add_router(
32-
cls, router: t.Union["ControllerBase", "ModuleRouter"], prefix: str
32+
cls,
33+
router: t.Union["ControllerBase", "ModuleRouter"],
34+
prefix: t.Optional[str] = None,
3335
) -> None:
3436
reflect.define_metadata(
3537
NESTED_ROUTERS_KEY, [NestedRouterInfo(prefix=prefix, router=router)], cls

ellar/common/operations/router.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ def control_type(self) -> t.Type[t.Any]:
5050
return self._control_type
5151

5252
def add_router(
53-
self, router: t.Union["ModuleRouter", "ControllerBase"], prefix: t.Optional[str]
53+
self,
54+
router: t.Union["ModuleRouter", "ControllerBase"],
55+
prefix: t.Optional[str] = None,
5456
) -> None:
5557
reflect.define_metadata(
5658
NESTED_ROUTERS_KEY,

ellar/core/router_builders/controller.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ def process_controller_routes(controller: t.Type[ControllerBase]) -> t.List[Base
5656
controller,
5757
)
5858
res.append(operation)
59-
reflect.define_metadata(CONTROLLER_METADATA.PROCESSED, True, controller)
6059
return res
6160

6261

@@ -101,6 +100,7 @@ def build(
101100
control_type=controller_type,
102101
**kwargs,
103102
)
103+
reflect.define_metadata(CONTROLLER_METADATA.PROCESSED, True, controller_type)
104104
return router
105105

106106
@classmethod

ellar/core/router_builders/module_router.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from ellar.common import ModuleRouter
44
from ellar.common.compatible import AttributeDict
5-
from ellar.common.constants import CONTROLLER_OPERATION_HANDLER_KEY
5+
from ellar.common.constants import CONTROLLER_METADATA, CONTROLLER_OPERATION_HANDLER_KEY
66
from ellar.common.shortcuts import normalize_path
77
from ellar.core.routing import EllarMount
88
from ellar.reflect import reflect
@@ -21,15 +21,17 @@ def build(
2121
base_route_type: t.Type[t.Union[EllarMount, T_]] = EllarMount,
2222
**kwargs: t.Any,
2323
) -> t.Union[T_, EllarMount]:
24-
if controller_type.get_pre_build_routes():
24+
if reflect.get_metadata(
25+
CONTROLLER_METADATA.PROCESSED, controller_type.control_type
26+
):
27+
routes = reflect.get_metadata(
28+
CONTROLLER_OPERATION_HANDLER_KEY, controller_type.control_type
29+
)
30+
else:
2531
routes = build_route_parameters(
2632
controller_type.get_pre_build_routes(), controller_type.control_type
2733
)
2834
routes.extend(process_nested_routes(controller_type.control_type))
29-
else:
30-
routes = reflect.get_metadata(
31-
CONTROLLER_OPERATION_HANDLER_KEY, controller_type.control_type
32-
)
3335

3436
init_kwargs = AttributeDict(controller_type.get_mount_init())
3537

@@ -38,7 +40,12 @@ def build(
3840
init_kwargs.path = normalize_path(f"{prefix}/{init_kwargs.path}")
3941

4042
controller_type.clear_pre_build_routes()
41-
return base_route_type(**init_kwargs, routes=routes) # type:ignore[call-arg]
43+
router = base_route_type(**init_kwargs, routes=routes) # type:ignore[call-arg]
44+
45+
reflect.define_metadata(
46+
CONTROLLER_METADATA.PROCESSED, True, controller_type.control_type
47+
)
48+
return router
4249

4350
@classmethod
4451
def check_type(cls, controller_type: t.Union[t.Type, t.Any]) -> None:

ellar/core/router_builders/utils.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
OPERATION_ENDPOINT_KEY,
1111
ROUTE_OPERATION_PARAMETERS,
1212
)
13+
from ellar.common.exceptions import ImproperConfiguration
1314
from ellar.common.logging import logger
1415
from ellar.common.models.controller import NestedRouterInfo
1516
from ellar.common.operations import RouteParameters, WsRouteParameters
@@ -26,6 +27,8 @@
2627

2728
T_ = t.TypeVar("T_")
2829

30+
_stack_cycle: t.Tuple = ()
31+
2932

3033
def get_route_functions(
3134
klass: t.Type, key: str = OPERATION_ENDPOINT_KEY
@@ -36,11 +39,20 @@ def get_route_functions(
3639

3740

3841
def process_nested_routes(controller: t.Type[t.Any]) -> t.List[BaseRoute]:
42+
global _stack_cycle
43+
3944
res: t.List[BaseRoute] = []
4045

4146
if reflect.get_metadata(CONTROLLER_METADATA.PROCESSED, controller):
4247
return []
4348

49+
if controller in _stack_cycle:
50+
raise ImproperConfiguration(
51+
"Circular Nested router detected: %s -> %s"
52+
% (" -> ".join(map(str, _stack_cycle)), controller)
53+
)
54+
55+
_stack_cycle += (controller,)
4456
nested_routers: t.List[NestedRouterInfo] = (
4557
reflect.get_metadata(NESTED_ROUTERS_KEY, controller) or []
4658
)
@@ -59,6 +71,8 @@ def process_nested_routes(controller: t.Type[t.Any]) -> t.List[BaseRoute]:
5971
)
6072

6173
res.append(operation)
74+
75+
_stack_cycle = tuple(_stack_cycle[:-1])
6276
return res
6377

6478

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import pytest
2+
from ellar.common import Controller, get
3+
from ellar.common.exceptions import ImproperConfiguration
4+
from ellar.testing import Test
5+
6+
7+
@Controller
8+
class Cat1Controller:
9+
@get("/create")
10+
async def create_cat(self):
11+
return {"message": f"created from {self.__class__.__name__}"}
12+
13+
14+
@Controller
15+
class Cat2Controller:
16+
@get("/create")
17+
async def create_cat(self):
18+
return {"message": f"created from {self.__class__.__name__}"}
19+
20+
21+
@Controller
22+
class Cat3Controller:
23+
@get("/create")
24+
async def create_cat(self):
25+
return {"message": f"created from {self.__class__.__name__}"}
26+
27+
28+
def test_can_reach_controllers():
29+
Cat2Controller.add_router(Cat1Controller)
30+
Cat3Controller.add_router(Cat2Controller)
31+
32+
tm = Test.create_test_module(controllers=[Cat3Controller])
33+
34+
client = tm.get_test_client()
35+
res = client.get("cat3/cat2/create")
36+
assert res.status_code == 200
37+
assert res.json() == {"message": "created from Cat2Controller"}
38+
39+
res = client.get("cat3/cat2/cat1/create")
40+
assert res.status_code == 200
41+
assert res.json() == {"message": "created from Cat1Controller"}
42+
43+
res = client.get("cat3/create")
44+
assert res.status_code == 200
45+
assert res.json() == {"message": "created from Cat3Controller"}
46+
47+
48+
def test_circular_exception_works():
49+
Cat2Controller.add_router(Cat1Controller)
50+
Cat1Controller.add_router(Cat2Controller)
51+
52+
with pytest.raises(ImproperConfiguration, match="Circular Nested router"):
53+
Test.create_test_module(controllers=[Cat2Controller]).create_application()
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
from ellar.common import Controller, get, serialize_object
2+
from ellar.openapi import OpenAPIDocumentBuilder
3+
from ellar.testing import Test
4+
5+
6+
@Controller
7+
class Cat1Controller:
8+
@get("/create")
9+
async def create_cat(self):
10+
return {"message": "created"}
11+
12+
13+
@Controller
14+
class Cat2Controller:
15+
@get("/create")
16+
async def create_cat(self):
17+
return {"message": "created"}
18+
19+
20+
Cat2Controller.add_router(Cat1Controller)
21+
22+
23+
tm = Test.create_test_module(controllers=[Cat2Controller])
24+
25+
26+
def test_nested_route_openapi_schema():
27+
app = tm.create_application()
28+
document = serialize_object(OpenAPIDocumentBuilder().build_document(app))
29+
assert document == NESTED_SCHEMA
30+
31+
32+
NESTED_SCHEMA = {
33+
"openapi": "3.1.0",
34+
"info": {"title": "Ellar API Docs", "version": "1.0.0"},
35+
"paths": {
36+
"/cat2/cat1/create": {
37+
"get": {
38+
"tags": ["cat1"],
39+
"operationId": "create_cat_create_get__cat1",
40+
"responses": {
41+
"200": {
42+
"description": "Successful Response",
43+
"content": {
44+
"application/json": {
45+
"schema": {"type": "object", "title": "Response Model"}
46+
}
47+
},
48+
}
49+
},
50+
}
51+
},
52+
"/cat2/create": {
53+
"get": {
54+
"tags": ["cat2"],
55+
"operationId": "create_cat_create_get__cat2",
56+
"responses": {
57+
"200": {
58+
"description": "Successful Response",
59+
"content": {
60+
"application/json": {
61+
"schema": {"type": "object", "title": "Response Model"}
62+
}
63+
},
64+
}
65+
},
66+
}
67+
},
68+
},
69+
"components": {
70+
"schemas": {
71+
"HTTPValidationError": {
72+
"properties": {
73+
"detail": {
74+
"items": {"$ref": "#/components/schemas/ValidationError"},
75+
"type": "array",
76+
"title": "Details",
77+
}
78+
},
79+
"type": "object",
80+
"required": ["detail"],
81+
"title": "HTTPValidationError",
82+
},
83+
"ValidationError": {
84+
"properties": {
85+
"loc": {
86+
"items": {"type": "string"},
87+
"type": "array",
88+
"title": "Location",
89+
},
90+
"msg": {"type": "string", "title": "Message"},
91+
"type": {"type": "string", "title": "Error Type"},
92+
},
93+
"type": "object",
94+
"required": ["loc", "msg", "type"],
95+
"title": "ValidationError",
96+
},
97+
}
98+
},
99+
"tags": [{"name": "cat1"}, {"name": "cat2"}],
100+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import pytest
2+
from ellar.common import ModuleRouter
3+
from ellar.common.exceptions import ImproperConfiguration
4+
from ellar.testing import Test
5+
6+
router1 = ModuleRouter("/cat1")
7+
8+
9+
@router1.get("/create")
10+
async def create_cat():
11+
return {"message": "created from ModuleRouter1"}
12+
13+
14+
router2 = ModuleRouter("/cat2")
15+
16+
17+
@router2.get("/create")
18+
async def create_cat_2():
19+
return {"message": "created from ModuleRouter2"}
20+
21+
22+
router3 = ModuleRouter("/cat3")
23+
24+
25+
@router3.get("/create")
26+
async def create_cat_3():
27+
return {"message": "created from ModuleRouter3"}
28+
29+
30+
def test_can_reach_routers():
31+
router2.add_router(router1)
32+
router3.add_router(router2)
33+
34+
tm = Test.create_test_module(routers=[router3])
35+
36+
client = tm.get_test_client()
37+
res = client.get("cat3/cat2/create")
38+
assert res.status_code == 200
39+
assert res.json() == {"message": "created from ModuleRouter2"}
40+
41+
res = client.get("cat3/cat2/cat1/create")
42+
assert res.status_code == 200
43+
assert res.json() == {"message": "created from ModuleRouter1"}
44+
45+
res = client.get("cat3/create")
46+
assert res.status_code == 200
47+
assert res.json() == {"message": "created from ModuleRouter3"}
48+
49+
50+
def test_circular_exception_works_router_reference():
51+
router2.add_router(router1)
52+
router1.add_router(router2)
53+
54+
with pytest.raises(ImproperConfiguration, match="Circular Nested router"):
55+
Test.create_test_module(routers=[router2]).create_application()

0 commit comments

Comments
 (0)