diff --git a/docs/api_controller/index.md b/docs/api_controller/index.md index 6400426..9689e25 100644 --- a/docs/api_controller/index.md +++ b/docs/api_controller/index.md @@ -43,7 +43,9 @@ The fourth argument, `permissions`, is a list of all permissions that should be The fifth argument, `auto_import`, defaults to true, which automatically adds your controller to auto import list. -The fifth argument, `urls_namespace`, defaults to `None`, or if set will nest the routes of this controller under their own namespace. +The sixth argument, `urls_namespace`, defaults to `Controller Class Name`, or if set will nest the routes of this controller under their own namespace. + +The seventh argument, `append_unique_op_id`, defaults to true, which appends a unique operation id suffix to the controller's routes to avoid conflicts. for example: diff --git a/ninja_extra/controllers/base.py b/ninja_extra/controllers/base.py index a96d47f..3c402c7 100644 --- a/ninja_extra/controllers/base.py +++ b/ninja_extra/controllers/base.py @@ -404,6 +404,7 @@ def __init__( permissions: Optional[List[BasePermissionType]] = None, auto_import: bool = True, urls_namespace: Optional[str] = None, + append_unique_op_id: bool = True ) -> None: self.prefix = prefix # Optional controller-level URL namespace. Applied to all route paths. @@ -413,6 +414,7 @@ def __init__( self.tags = tags # type: ignore self.throttle = throttle + self.append_unique_op_id = append_unique_op_id self.auto_import: bool = auto_import # set to false and it would be ignored when api.auto_discover is called # `controller_class` target class that the APIController wraps @@ -597,7 +599,13 @@ def _add_operation_from_route_function(self, route_function: RouteFunction) -> N controller_name = ( str(self.controller_class.__name__).lower().replace("controller", "") ) - route_function.route.route_params.operation_id = f"{controller_name}_{route_function.route.view_func.__name__}_{str(uuid.uuid4())[:8]}" + route_function.route.route_params.operation_id = ( + f"{controller_name}_{route_function.route.view_func.__name__}" + ) + if self.append_unique_op_id: + route_function.route.route_params.operation_id += ( + f"_{uuid.uuid4().hex[:8]}" + ) if ( self.auth @@ -687,6 +695,7 @@ def api_controller( permissions: Optional[List[BasePermissionType]] = None, auto_import: bool = True, urls_namespace: Optional[str] = None, + append_unique_op_id: bool = True, ) -> Callable[ [Union[Type, Type[T]]], Union[Type[ControllerBase], Type[T]] ]: # pragma: no cover @@ -701,6 +710,7 @@ def api_controller( permissions: Optional[List[BasePermissionType]] = None, auto_import: bool = True, urls_namespace: Optional[str] = None, + append_unique_op_id: bool = True, ) -> Union[ControllerClassType, Callable[[ControllerClassType], ControllerClassType]]: if isinstance(prefix_or_class, type): return APIController( @@ -710,6 +720,7 @@ def api_controller( permissions=permissions, auto_import=auto_import, throttle=throttle, + append_unique_op_id=append_unique_op_id, urls_namespace=urls_namespace, )(prefix_or_class) @@ -721,6 +732,7 @@ def _decorator(cls: ControllerClassType) -> ControllerClassType: permissions=permissions, auto_import=auto_import, throttle=throttle, + append_unique_op_id=append_unique_op_id, urls_namespace=urls_namespace, )(cls) diff --git a/tests/test_controller.py b/tests/test_controller.py index 377072f..93f2164 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -68,6 +68,20 @@ class DisableAutoImportController: auto_import = False # disable auto_import of the controller +@api_controller +class SomeControllerWithSingleRoute: + @http_get("/example") + def example(self): + pass + + +@api_controller(append_unique_op_id=False) +class SomeControllerWithoutUniqueSuffix: + @http_get("/example") + def example(self): + pass + + class TestAPIController: def test_api_controller_as_decorator(self): controller_type = api_controller("prefix", tags="new_tag", auth=FakeAuth())( @@ -144,6 +158,43 @@ def test_controller_should_have_path_operation_list(self): assert operation.methods == route_function.route.route_params.methods assert operation.operation_id == route_function.route.route_params.operation_id + def test_controller_should_append_unique_op_id_to_operation_id(self): + _api_controller = SomeControllerWithSingleRoute.get_api_controller() + controller_name = ( + str(_api_controller.controller_class.__name__) + .lower() + .replace("controller", "") + ) + route_view_func_name: RouteFunction = get_route_function( + SomeControllerWithRoute().example + ).route.view_func.__name__ + + operation_id = ( + _api_controller._path_operations.get("/example").operations[0].operation_id + ) + raw_operation_id = "_".join(operation_id.split("_")[:-1]) + op_id_postfix = operation_id.split("_")[-1] + + assert raw_operation_id == f"{controller_name}_{route_view_func_name}" + assert len(op_id_postfix) == 8 + + def test_controller_should_not_add_unique_suffix_following_params(self): + _api_controller = SomeControllerWithoutUniqueSuffix.get_api_controller() + controller_name = ( + str(_api_controller.controller_class.__name__) + .lower() + .replace("controller", "") + ) + route_view_func_name: RouteFunction = get_route_function( + SomeControllerWithRoute().example + ).route.view_func.__name__ + + operation_id = ( + _api_controller._path_operations.get("/example").operations[0].operation_id + ) + + assert operation_id == f"{controller_name}_{route_view_func_name}" + def test_get_route_function_should_return_instance_route_definitions(self): for route_definition in get_route_functions(SomeControllerWithRoute): assert isinstance(route_definition, RouteFunction)