From 3e085f009efdd29e8e8518ad388a974e8a72022c Mon Sep 17 00:00:00 2001 From: Martin Desrumaux Date: Fri, 28 Feb 2020 17:29:58 +0100 Subject: [PATCH 1/2] feat(openapi/operationId): Make camel-case --- rest_framework/schemas/openapi.py | 18 ++++++++++++------ tests/schemas/test_openapi.py | 19 ++++++++++++++++++- tests/schemas/views.py | 24 +++++++++++++++++++++++- 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index 6bed120922..1d0ec35d54 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -131,11 +131,11 @@ def __init__(self, tags=None, operation_id_base=None, component_name=None): response_media_types = [] method_mapping = { - 'get': 'Retrieve', - 'post': 'Create', - 'put': 'Update', - 'patch': 'PartialUpdate', - 'delete': 'Destroy', + 'get': 'retrieve', + 'post': 'create', + 'put': 'update', + 'patch': 'partialUpdate', + 'delete': 'destroy', } def get_operation(self, path, method): @@ -195,6 +195,12 @@ def get_components(self, path, method): content = self._map_serializer(serializer) return {component_name: content} + def _to_camel_case(self, snake_str): + components = snake_str.split('_') + # We capitalize the first letter of each component except the first one + # with the 'title' method and join them together. + return components[0] + ''.join(x.title() for x in components[1:]) + def get_operation_id_base(self, path, method, action): """ Compute the base part for operation ID from the model, serializer or view name. @@ -240,7 +246,7 @@ def get_operation_id(self, path, method): if is_list_view(path, method, self.view): action = 'list' elif method_name not in self.method_mapping: - action = method_name + action = self._to_camel_case(method_name) else: action = self.method_mapping[method.lower()] diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 35d676d6c3..c9f6d967ec 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -158,7 +158,7 @@ def test_path_with_id_parameter(self): operation = inspector.get_operation(path, method) assert operation == { - 'operationId': 'RetrieveDocStringExampleDetail', + 'operationId': 'retrieveDocStringExampleDetail', 'description': 'A description of my GET operation.', 'parameters': [{ 'description': '', @@ -735,6 +735,23 @@ def test_duplicate_operation_id(self): print(str(w[-1].message)) assert 'You have a duplicated operationId' in str(w[-1].message) + def test_operation_id_viewset(self): + router = routers.SimpleRouter() + router.register('account', views.ExampleViewSet, basename="account") + urlpatterns = router.urls + + generator = SchemaGenerator(patterns=urlpatterns) + + request = create_request('/') + schema = generator.get_schema(request=request) + print(schema) + assert schema['paths']['/account/']['get']['operationId'] == 'listExampleViewSets' + assert schema['paths']['/account/']['post']['operationId'] == 'createExampleViewSet' + assert schema['paths']['/account/{id}/']['get']['operationId'] == 'retrieveExampleViewSet' + assert schema['paths']['/account/{id}/']['put']['operationId'] == 'updateExampleViewSet' + assert schema['paths']['/account/{id}/']['patch']['operationId'] == 'partialUpdateExampleViewSet' + assert schema['paths']['/account/{id}/']['delete']['operationId'] == 'destroyExampleViewSet' + def test_serializer_datefield(self): path = '/' method = 'GET' diff --git a/tests/schemas/views.py b/tests/schemas/views.py index 1c8235b425..5645f59bf5 100644 --- a/tests/schemas/views.py +++ b/tests/schemas/views.py @@ -11,7 +11,7 @@ from rest_framework.response import Response from rest_framework.schemas.openapi import AutoSchema from rest_framework.views import APIView -from rest_framework.viewsets import GenericViewSet +from rest_framework.viewsets import GenericViewSet, ViewSet class ExampleListView(APIView): @@ -215,3 +215,25 @@ def get(self, *args, **kwargs): serializer = self.get_serializer(data=now.date(), datetime=now) return Response(serializer.data) + + +class ExampleViewSet(ViewSet): + serializer_class = ExampleSerializerModel + + def list(self, request): + pass + + def create(self, request): + pass + + def retrieve(self, request, pk=None): + pass + + def update(self, request, pk=None): + pass + + def partial_update(self, request, pk=None): + pass + + def destroy(self, request, pk=None): + pass From d1c849e28269fdb26e60dda04e3a13b759aa7ef6 Mon Sep 17 00:00:00 2001 From: Martin Desrumaux Date: Mon, 2 Mar 2020 18:49:49 +0100 Subject: [PATCH 2/2] fix(docs): Replace PascalCase with camelCase --- docs/api-guide/schemas.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md index 1d1e09b46a..2d74882ade 100644 --- a/docs/api-guide/schemas.md +++ b/docs/api-guide/schemas.md @@ -290,7 +290,8 @@ 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.. +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.. +The `operationId` is camelCase by convention. 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. @@ -303,7 +304,7 @@ class ExampleView(APIView): schema = AutoSchema(operation_id_base="Custom") ``` -The previous example will generate the following operationId: "ListCustoms", "RetrieveCustom", "UpdateCustom", "PartialUpdateCustom", "DestroyCustom". +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: