Skip to content

Commit 326372b

Browse files
committed
ft: Added nested routing support for ModuleRouters and Controllers
1 parent 664a8f5 commit 326372b

File tree

17 files changed

+168
-92
lines changed

17 files changed

+168
-92
lines changed

ellar/common/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
TEMPLATE_GLOBAL_KEY = "TEMPLATE_GLOBAL_FILTERS"
3535
TEMPLATE_FILTER_KEY = "TEMPLATE_FILTERS"
36+
NESTED_ROUTERS_KEY = "NESTED_ROUTERS_KEY"
3637

3738
MIDDLEWARE_HANDLERS_KEY = "MIDDLEWARE"
3839

ellar/common/models/controller.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
1+
import dataclasses
12
import typing as t
23

3-
from ..interfaces import IExecutionContext
4+
from ellar.common.constants import NESTED_ROUTERS_KEY
5+
from ellar.common.interfaces import IExecutionContext
6+
from ellar.reflect import reflect
7+
8+
if t.TYPE_CHECKING:
9+
from ellar.common.operations import ModuleRouter
10+
11+
12+
@dataclasses.dataclass
13+
class NestedRouterInfo:
14+
router: t.Union["ModuleRouter", "ControllerBase"]
15+
prefix: t.Optional[str] = None
416

517

618
class ControllerType(type):
@@ -16,6 +28,13 @@ def full_view_name(cls, name: str) -> str:
1628
""" """
1729
return f"{cls.controller_class_name()}/{name}"
1830

31+
def add_router(
32+
cls, router: t.Union["ControllerBase", "ModuleRouter"], prefix: str
33+
) -> None:
34+
reflect.define_metadata(
35+
NESTED_ROUTERS_KEY, [NestedRouterInfo(prefix=prefix, router=router)], cls
36+
)
37+
1938

2039
class ControllerBase(metaclass=ControllerType):
2140
# `context` variable will change based on the route function called on the APIController

ellar/common/operations/router.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
import typing as t
22

3-
from ellar.common.constants import CONTROLLER_CLASS_KEY, GUARDS_KEY, VERSIONING_KEY
3+
from ellar.common.constants import (
4+
CONTROLLER_CLASS_KEY,
5+
GUARDS_KEY,
6+
NESTED_ROUTERS_KEY,
7+
VERSIONING_KEY,
8+
)
49
from ellar.common.models import GuardCanActivate
10+
from ellar.common.models.controller import NestedRouterInfo
511
from ellar.reflect import reflect
612
from ellar.utils import get_unique_type
713
from starlette.middleware import Middleware
814

915
from .base import OperationDefinitions
1016
from .schema import RouteParameters, WsRouteParameters
1117

18+
if t.TYPE_CHECKING: # pragma: no cover
19+
from ellar.common import ControllerBase
20+
1221

1322
class ModuleRouter(OperationDefinitions):
1423
def __init__(
@@ -40,6 +49,15 @@ def __init__(
4049
def control_type(self) -> t.Type[t.Any]:
4150
return self._control_type
4251

52+
def add_router(
53+
self, router: t.Union["ModuleRouter", "ControllerBase"], prefix: t.Optional[str]
54+
) -> None:
55+
reflect.define_metadata(
56+
NESTED_ROUTERS_KEY,
57+
[NestedRouterInfo(prefix=prefix, router=router)],
58+
self.control_type,
59+
)
60+
4361
def get_mount_init(self) -> t.Dict[str, t.Any]:
4462
return self._kwargs.copy()
4563

ellar/common/operations/schema.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
model_validator,
1616
)
1717

18+
if t.TYPE_CHECKING:
19+
pass
20+
1821

1922
@as_pydantic_validator("__validate_input__")
2023
class TResponseModel:

ellar/core/router_builders/base.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@
22
from abc import abstractmethod
33

44
from ellar.common.logging import logger
5+
from ellar.core.routing.mount import EllarMount
56
from starlette.routing import Host, Mount
67

7-
if t.TYPE_CHECKING: # pragma: no cover
8-
from ellar.core.routing.mount import EllarMount
9-
108
_router_builder_factory: t.Dict[t.Type, t.Type["RouterBuilder"]] = {}
119

1210

@@ -66,3 +64,4 @@ def build(
6664

6765

6866
_register_controller_builder(Host, _DefaultRouterBuilder)
67+
_register_controller_builder(EllarMount, _DefaultRouterBuilder)

ellar/core/router_builders/controller.py

Lines changed: 48 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
import inspect
21
import typing as t
32

43
from ellar.common.constants import (
54
CONTROLLER_METADATA,
65
CONTROLLER_OPERATION_HANDLER_KEY,
76
CONTROLLER_WATERMARK,
8-
OPERATION_ENDPOINT_KEY,
97
ROUTE_OPERATION_PARAMETERS,
108
)
119
from ellar.common.logging import logger
12-
from ellar.common.models import ControllerBase, ControllerType
10+
from ellar.common.models.controller import ControllerBase, ControllerType
1311
from ellar.common.operations import RouteParameters, WsRouteParameters
12+
from ellar.common.shortcuts import normalize_path
1413
from ellar.core.routing import (
1514
ControllerRouteOperation,
1615
ControllerWebsocketRouteOperation,
@@ -20,72 +19,57 @@
2019
from starlette.routing import BaseRoute
2120

2221
from .base import RouterBuilder
22+
from .utils import get_route_functions, process_nested_routes
2323

2424
T_ = t.TypeVar("T_")
2525

2626

27-
class ControllerRouterBuilder(RouterBuilder, controller_type=type(ControllerBase)):
28-
@classmethod
29-
def _get_route_functions(
30-
cls,
31-
klass: t.Type,
32-
) -> t.Iterable[t.Callable]:
33-
for _method_name, method in inspect.getmembers(
34-
klass, predicate=inspect.isfunction
35-
):
36-
if hasattr(method, OPERATION_ENDPOINT_KEY):
37-
yield method
27+
def process_controller_routes(controller: t.Type[ControllerBase]) -> t.List[BaseRoute]:
28+
res: t.List[BaseRoute] = []
3829

39-
@classmethod
40-
def _process_controller_routes(
41-
cls, controller: t.Type[ControllerBase]
42-
) -> t.Sequence[BaseRoute]:
43-
res = []
44-
45-
if reflect.get_metadata(CONTROLLER_METADATA.PROCESSED, controller):
46-
return (
47-
reflect.get_metadata(CONTROLLER_OPERATION_HANDLER_KEY, controller) or []
48-
)
30+
if reflect.get_metadata(CONTROLLER_METADATA.PROCESSED, controller):
31+
return reflect.get_metadata(CONTROLLER_OPERATION_HANDLER_KEY, controller) or []
4932

50-
for item in cls._get_route_functions(controller):
51-
parameters = item.__dict__[ROUTE_OPERATION_PARAMETERS]
52-
operation: t.Union[
53-
ControllerRouteOperation, ControllerWebsocketRouteOperation
54-
]
55-
56-
if not isinstance(parameters, list):
57-
parameters = [parameters]
58-
59-
for parameter in parameters:
60-
if isinstance(parameter, RouteParameters):
61-
operation = ControllerRouteOperation(controller, **parameter.dict())
62-
elif isinstance(parameter, WsRouteParameters):
63-
operation = ControllerWebsocketRouteOperation(
64-
controller, **parameter.dict()
65-
)
66-
else: # pragma: no cover
67-
logger.warning(
68-
f"Parameter type is not recognized. {type(parameter) if not isinstance(parameter, type) else parameter}"
69-
)
70-
continue
71-
72-
reflect.define_metadata(
73-
CONTROLLER_OPERATION_HANDLER_KEY,
74-
[operation],
75-
controller,
33+
for _, item in get_route_functions(controller):
34+
parameters = item.__dict__[ROUTE_OPERATION_PARAMETERS]
35+
operation: t.Union[ControllerRouteOperation, ControllerWebsocketRouteOperation]
36+
37+
if not isinstance(parameters, list):
38+
parameters = [parameters]
39+
40+
for parameter in parameters:
41+
if isinstance(parameter, RouteParameters):
42+
operation = ControllerRouteOperation(controller, **parameter.dict())
43+
elif isinstance(parameter, WsRouteParameters):
44+
operation = ControllerWebsocketRouteOperation(
45+
controller, **parameter.dict()
7646
)
77-
res.append(operation)
78-
reflect.define_metadata(CONTROLLER_METADATA.PROCESSED, True, controller)
79-
return res
47+
else: # pragma: no cover
48+
logger.warning(
49+
f"Parameter type is not recognized. {type(parameter) if not isinstance(parameter, type) else parameter}"
50+
)
51+
continue
8052

53+
reflect.define_metadata(
54+
CONTROLLER_OPERATION_HANDLER_KEY,
55+
[operation],
56+
controller,
57+
)
58+
res.append(operation)
59+
reflect.define_metadata(CONTROLLER_METADATA.PROCESSED, True, controller)
60+
return res
61+
62+
63+
class ControllerRouterBuilder(RouterBuilder, controller_type=type(ControllerBase)):
8164
@classmethod
8265
def build(
8366
cls,
8467
controller_type: t.Union[t.Type[ControllerBase], t.Any],
8568
base_route_type: t.Type[t.Union[EllarMount, T_]] = EllarMount,
8669
**kwargs: t.Any,
8770
) -> t.Union[T_, EllarMount]:
88-
routes = cls._process_controller_routes(controller_type)
71+
routes = process_controller_routes(controller_type)
72+
routes.extend(process_nested_routes(controller_type))
8973

9074
include_in_schema = reflect.get_metadata_or_raise_exception(
9175
CONTROLLER_METADATA.INCLUDE_IN_SCHEMA, controller_type
@@ -96,11 +80,18 @@ def build(
9680
)
9781

9882
kwargs.setdefault("middleware", middleware)
83+
84+
path = reflect.get_metadata_or_raise_exception(
85+
CONTROLLER_METADATA.PATH, controller_type
86+
)
87+
88+
if "prefix" in kwargs:
89+
prefix = kwargs.pop("prefix")
90+
path = normalize_path(f"{prefix}/{path}")
91+
9992
router = base_route_type( # type:ignore[call-arg]
10093
routes=routes,
101-
path=reflect.get_metadata_or_raise_exception(
102-
CONTROLLER_METADATA.PATH, controller_type
103-
),
94+
path=path,
10495
name=reflect.get_metadata_or_raise_exception(
10596
CONTROLLER_METADATA.NAME, controller_type
10697
),

ellar/core/router_builders/module_router.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import typing as t
22

33
from ellar.common import ModuleRouter
4+
from ellar.common.compatible import AttributeDict
45
from ellar.common.constants import CONTROLLER_OPERATION_HANDLER_KEY
6+
from ellar.common.shortcuts import normalize_path
57
from ellar.core.routing import EllarMount
6-
from ellar.core.routing.utils import build_route_parameters
78
from ellar.reflect import reflect
89

910
from .base import RouterBuilder
11+
from .utils import build_route_parameters, process_nested_routes
1012

1113
T_ = t.TypeVar("T_")
1214

@@ -23,13 +25,20 @@ def build(
2325
routes = build_route_parameters(
2426
controller_type.get_pre_build_routes(), controller_type.control_type
2527
)
28+
routes.extend(process_nested_routes(controller_type.control_type))
2629
else:
2730
routes = reflect.get_metadata(
2831
CONTROLLER_OPERATION_HANDLER_KEY, controller_type.control_type
2932
)
3033

34+
init_kwargs = AttributeDict(controller_type.get_mount_init())
35+
36+
if "prefix" in kwargs:
37+
prefix = kwargs.pop("prefix")
38+
init_kwargs.path = normalize_path(f"{prefix}/{init_kwargs.path}")
39+
3140
controller_type.clear_pre_build_routes()
32-
return base_route_type(**controller_type.get_mount_init(), routes=routes) # type:ignore[call-arg]
41+
return base_route_type(**init_kwargs, routes=routes) # type:ignore[call-arg]
3342

3443
@classmethod
3544
def check_type(cls, controller_type: t.Union[t.Type, t.Any]) -> None:
Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,65 @@
1+
import inspect
12
import typing as t
23
from types import FunctionType
34

45
from ellar.common.constants import (
56
CONTROLLER_CLASS_KEY,
7+
CONTROLLER_METADATA,
68
CONTROLLER_OPERATION_HANDLER_KEY,
9+
NESTED_ROUTERS_KEY,
10+
OPERATION_ENDPOINT_KEY,
711
ROUTE_OPERATION_PARAMETERS,
812
)
913
from ellar.common.logging import logger
14+
from ellar.common.models.controller import NestedRouterInfo
1015
from ellar.common.operations import RouteParameters, WsRouteParameters
16+
from ellar.core.routing import (
17+
RouteOperation,
18+
RouteOperationBase,
19+
WebsocketRouteOperation,
20+
)
1121
from ellar.reflect import reflect
1222
from ellar.utils import get_unique_type
23+
from starlette.routing import BaseRoute
24+
25+
from .base import get_controller_builder_factory
26+
27+
T_ = t.TypeVar("T_")
28+
29+
30+
def get_route_functions(
31+
klass: t.Type, key: str = OPERATION_ENDPOINT_KEY
32+
) -> t.Iterable[t.Tuple[str, t.Callable]]:
33+
for method_name, method in inspect.getmembers(klass, predicate=inspect.isfunction):
34+
if hasattr(method, key):
35+
yield method_name, method
36+
37+
38+
def process_nested_routes(controller: t.Type[t.Any]) -> t.List[BaseRoute]:
39+
res: t.List[BaseRoute] = []
40+
41+
if reflect.get_metadata(CONTROLLER_METADATA.PROCESSED, controller):
42+
return []
43+
44+
nested_routers: t.List[NestedRouterInfo] = (
45+
reflect.get_metadata(NESTED_ROUTERS_KEY, controller) or []
46+
)
47+
48+
for item in nested_routers:
49+
kw = {"prefix": item.prefix} if item.prefix else {}
50+
51+
factory_builder = get_controller_builder_factory(type(item.router))
52+
factory_builder.check_type(item.router)
53+
54+
operation = factory_builder.build(item.router, **kw)
55+
reflect.define_metadata(
56+
CONTROLLER_OPERATION_HANDLER_KEY,
57+
[operation],
58+
controller,
59+
)
1360

14-
from .base import RouteOperationBase
15-
from .route import RouteOperation
16-
from .websocket import WebsocketRouteOperation
61+
res.append(operation)
62+
return res
1763

1864

1965
@t.no_type_check
@@ -48,7 +94,7 @@ def build_route_handler(
4894
def build_route_parameters(
4995
items: t.List[t.Union[RouteParameters, WsRouteParameters]],
5096
control_type: t.Type[t.Any],
51-
) -> t.List[RouteOperationBase]:
97+
) -> t.List[t.Union[RouteOperationBase, t.Any]]:
5298
results = []
5399
for item in items:
54100
if isinstance(item, RouteParameters):

0 commit comments

Comments
 (0)