OpenAPI: Make operationId camelCase, matching spec examples. (#7208)

This commit is contained in:
Martin Desrumaux 2020-03-03 17:51:51 +01:00 committed by GitHub
parent 609f708a27
commit 6a23fa0649
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 56 additions and 10 deletions

View File

@ -290,7 +290,8 @@ class MyView(APIView):
### OperationId ### 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. 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. 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") 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. 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: 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:

View File

@ -131,11 +131,11 @@ class AutoSchema(ViewInspector):
response_media_types = [] response_media_types = []
method_mapping = { method_mapping = {
'get': 'Retrieve', 'get': 'retrieve',
'post': 'Create', 'post': 'create',
'put': 'Update', 'put': 'update',
'patch': 'PartialUpdate', 'patch': 'partialUpdate',
'delete': 'Destroy', 'delete': 'destroy',
} }
def get_operation(self, path, method): def get_operation(self, path, method):
@ -195,6 +195,12 @@ class AutoSchema(ViewInspector):
content = self._map_serializer(serializer) content = self._map_serializer(serializer)
return {component_name: content} 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): def get_operation_id_base(self, path, method, action):
""" """
Compute the base part for operation ID from the model, serializer or view name. Compute the base part for operation ID from the model, serializer or view name.
@ -240,7 +246,7 @@ class AutoSchema(ViewInspector):
if is_list_view(path, method, self.view): if is_list_view(path, method, self.view):
action = 'list' action = 'list'
elif method_name not in self.method_mapping: elif method_name not in self.method_mapping:
action = method_name action = self._to_camel_case(method_name)
else: else:
action = self.method_mapping[method.lower()] action = self.method_mapping[method.lower()]

View File

@ -158,7 +158,7 @@ class TestOperationIntrospection(TestCase):
operation = inspector.get_operation(path, method) operation = inspector.get_operation(path, method)
assert operation == { assert operation == {
'operationId': 'RetrieveDocStringExampleDetail', 'operationId': 'retrieveDocStringExampleDetail',
'description': 'A description of my GET operation.', 'description': 'A description of my GET operation.',
'parameters': [{ 'parameters': [{
'description': '', 'description': '',
@ -735,6 +735,23 @@ class TestOperationIntrospection(TestCase):
print(str(w[-1].message)) print(str(w[-1].message))
assert 'You have a duplicated operationId' in 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): def test_serializer_datefield(self):
path = '/' path = '/'
method = 'GET' method = 'GET'

View File

@ -11,7 +11,7 @@ from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.schemas.openapi import AutoSchema from rest_framework.schemas.openapi import AutoSchema
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet, ViewSet
class ExampleListView(APIView): class ExampleListView(APIView):
@ -215,3 +215,25 @@ class ExampleAutoSchemaDuplicate2(generics.GenericAPIView):
serializer = self.get_serializer(data=now.date(), datetime=now) serializer = self.get_serializer(data=now.date(), datetime=now)
return Response(serializer.data) 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