diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md index 2e5ffc79b8..5766a6a61c 100644 --- a/docs/api-guide/schemas.md +++ b/docs/api-guide/schemas.md @@ -288,8 +288,41 @@ class MyView(APIView): ... ``` +### OperationId + +The schema generator generates an [operationid](openapi-operationid) for each operation. This `operationId` is deduced from the model name, serializer name or view name. The operationId may looks like "ListItems", "RetrieveItem", "UpdateItem", etc.. + +If you have several views with the same model, the generator may generate duplicate operationId. +In order to work around this, you can override the second part of the operationId: operation name. + +```python +from rest_framework.schemas.openapi import AutoSchema + +class ExampleView(APIView): + """APIView subclass with custom schema introspection.""" + schema = AutoSchema(operation_id_base="Custom") +``` + +The previous example will generate the following operationId: "ListCustoms", "RetrieveCustom", "UpdateCustom", "PartialUpdateCustom", "DestroyCustom". +You need to provide the singular form of he operation name. For the list operation, a "s" will be appended at the end of the operation. + +If you need more configuration over the `operationId` field, you can override the `get_operation_id_base` and `get_operation_id` methods from the `AutoSchema` class: + +```python +class CustomSchema(AutoSchema): + def get_operation_id_base(self, path, method, action): + pass + + def get_operation_id(self, path, method): + pass + +class CustomView(APIView): + """APIView subclass with custom schema introspection.""" + schema = CustomSchema() +``` [openapi]: https://github.com/OAI/OpenAPI-Specification [openapi-specification-extensions]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#specification-extensions [openapi-operation]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operationObject [openapi-tags]: https://swagger.io/specification/#tagObject +[openapi-operationid]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#fixed-fields-17 diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index 5277f17a61..5cbba1f9a3 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -71,10 +71,14 @@ def get_schema(self, request=None, public=False): class AutoSchema(ViewInspector): - def __init__(self, tags=None): + def __init__(self, operation_id_base=None, tags=None): + """ + :param operation_id_base: user-defined name in operationId. If empty, it will be deducted from the Model/Serializer/View name. + """ if tags and not all(isinstance(tag, str) for tag in tags): raise ValueError('tags must be a list or tuple of string.') self._tags = tags + self.operation_id_base = operation_id_base super().__init__() request_media_types = [] @@ -91,7 +95,7 @@ def __init__(self, tags=None): def get_operation(self, path, method): operation = {} - operation['operationId'] = self._get_operation_id(path, method) + operation['operationId'] = self.get_operation_id(path, method) operation['description'] = self.get_description(path, method) parameters = [] @@ -108,21 +112,17 @@ def get_operation(self, path, method): return operation - def _get_operation_id(self, path, method): + def get_operation_id_base(self, path, method, action): """ - Compute an operation ID from the model, serializer or view name. + Compute the base part for operation ID from the model, serializer or view name. """ - method_name = getattr(self.view, 'action', method.lower()) - if is_list_view(path, method, self.view): - action = 'list' - elif method_name not in self.method_mapping: - action = method_name - else: - action = self.method_mapping[method.lower()] + model = getattr(getattr(self.view, 'queryset', None), 'model', None) + + if self.operation_id_base is not None: + name = self.operation_id_base # Try to deduce the ID from the view's model - model = getattr(getattr(self.view, 'queryset', None), 'model', None) - if model is not None: + elif model is not None: name = model.__name__ # Try with the serializer class name @@ -147,6 +147,22 @@ def _get_operation_id(self, path, method): if action == 'list' and not name.endswith('s'): # listThings instead of listThing name += 's' + return name + + def get_operation_id(self, path, method): + """ + Compute an operation ID from the view type and get_operation_id_base method. + """ + method_name = getattr(self.view, 'action', method.lower()) + if is_list_view(path, method, self.view): + action = 'list' + elif method_name not in self.method_mapping: + action = method_name + else: + action = self.method_mapping[method.lower()] + + name = self.get_operation_id_base(path, method, action) + return action + name def _get_path_parameters(self, path, method): diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 7f73c8c300..ab206198ed 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -574,9 +574,75 @@ def test_operation_id_generation(self): inspector = AutoSchema() inspector.view = view - operationId = inspector._get_operation_id(path, method) + operationId = inspector.get_operation_id(path, method) assert operationId == 'listExamples' + def test_operation_id_custom_operation_id_base(self): + path = '/' + method = 'GET' + + view = create_view( + views.ExampleGenericAPIView, + method, + create_request(path), + ) + inspector = AutoSchema(operation_id_base="Ulysse") + inspector.view = view + + operationId = inspector.get_operation_id(path, method) + assert operationId == 'listUlysses' + + def test_operation_id_custom_name(self): + path = '/' + method = 'GET' + + view = create_view( + views.ExampleGenericAPIView, + method, + create_request(path), + ) + inspector = AutoSchema(operation_id_base='Ulysse') + inspector.view = view + + operationId = inspector.get_operation_id(path, method) + assert operationId == 'listUlysses' + + def test_operation_id_override_get(self): + class CustomSchema(AutoSchema): + def get_operation_id(self, path, method): + return 'myCustomOperationId' + + path = '/' + method = 'GET' + view = create_view( + views.ExampleGenericAPIView, + method, + create_request(path), + ) + inspector = CustomSchema() + inspector.view = view + + operationId = inspector.get_operation_id(path, method) + assert operationId == 'myCustomOperationId' + + def test_operation_id_override_base(self): + class CustomSchema(AutoSchema): + def get_operation_id_base(self, path, method, action): + return 'Item' + + path = '/' + method = 'GET' + view = create_view( + views.ExampleGenericAPIView, + method, + create_request(path), + ) + inspector = CustomSchema() + inspector.view = view + + operationId = inspector.get_operation_id(path, method) + assert operationId == 'listItem' + def test_repeat_operation_ids(self): router = routers.SimpleRouter() router.register('account', views.ExampleGenericViewSet, basename="account")