Skip to content

Commit 6a23fa0

Browse files
authored
OpenAPI: Make operationId camelCase, matching spec examples. (#7208)
1 parent 609f708 commit 6a23fa0

File tree

4 files changed

+56
-10
lines changed

4 files changed

+56
-10
lines changed

docs/api-guide/schemas.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,8 @@ class MyView(APIView):
290290

291291
### OperationId
292292

293-
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..
293+
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..
294+
The `operationId` is camelCase by convention.
294295

295296
If you have several views with the same model, the generator may generate duplicate operationId.
296297
In order to work around this, you can override the second part of the operationId: operation name.
@@ -303,7 +304,7 @@ class ExampleView(APIView):
303304
schema = AutoSchema(operation_id_base="Custom")
304305
```
305306

306-
The previous example will generate the following operationId: "ListCustoms", "RetrieveCustom", "UpdateCustom", "PartialUpdateCustom", "DestroyCustom".
307+
The previous example will generate the following operationId: "listCustoms", "retrieveCustom", "updateCustom", "partialUpdateCustom", "destroyCustom".
307308
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.
308309

309310
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:

rest_framework/schemas/openapi.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,11 @@ def __init__(self, tags=None, operation_id_base=None, component_name=None):
131131
response_media_types = []
132132

133133
method_mapping = {
134-
'get': 'Retrieve',
135-
'post': 'Create',
136-
'put': 'Update',
137-
'patch': 'PartialUpdate',
138-
'delete': 'Destroy',
134+
'get': 'retrieve',
135+
'post': 'create',
136+
'put': 'update',
137+
'patch': 'partialUpdate',
138+
'delete': 'destroy',
139139
}
140140

141141
def get_operation(self, path, method):
@@ -195,6 +195,12 @@ def get_components(self, path, method):
195195
content = self._map_serializer(serializer)
196196
return {component_name: content}
197197

198+
def _to_camel_case(self, snake_str):
199+
components = snake_str.split('_')
200+
# We capitalize the first letter of each component except the first one
201+
# with the 'title' method and join them together.
202+
return components[0] + ''.join(x.title() for x in components[1:])
203+
198204
def get_operation_id_base(self, path, method, action):
199205
"""
200206
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):
240246
if is_list_view(path, method, self.view):
241247
action = 'list'
242248
elif method_name not in self.method_mapping:
243-
action = method_name
249+
action = self._to_camel_case(method_name)
244250
else:
245251
action = self.method_mapping[method.lower()]
246252

tests/schemas/test_openapi.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ def test_path_with_id_parameter(self):
158158

159159
operation = inspector.get_operation(path, method)
160160
assert operation == {
161-
'operationId': 'RetrieveDocStringExampleDetail',
161+
'operationId': 'retrieveDocStringExampleDetail',
162162
'description': 'A description of my GET operation.',
163163
'parameters': [{
164164
'description': '',
@@ -735,6 +735,23 @@ def test_duplicate_operation_id(self):
735735
print(str(w[-1].message))
736736
assert 'You have a duplicated operationId' in str(w[-1].message)
737737

738+
def test_operation_id_viewset(self):
739+
router = routers.SimpleRouter()
740+
router.register('account', views.ExampleViewSet, basename="account")
741+
urlpatterns = router.urls
742+
743+
generator = SchemaGenerator(patterns=urlpatterns)
744+
745+
request = create_request('/')
746+
schema = generator.get_schema(request=request)
747+
print(schema)
748+
assert schema['paths']['/account/']['get']['operationId'] == 'listExampleViewSets'
749+
assert schema['paths']['/account/']['post']['operationId'] == 'createExampleViewSet'
750+
assert schema['paths']['/account/{id}/']['get']['operationId'] == 'retrieveExampleViewSet'
751+
assert schema['paths']['/account/{id}/']['put']['operationId'] == 'updateExampleViewSet'
752+
assert schema['paths']['/account/{id}/']['patch']['operationId'] == 'partialUpdateExampleViewSet'
753+
assert schema['paths']['/account/{id}/']['delete']['operationId'] == 'destroyExampleViewSet'
754+
738755
def test_serializer_datefield(self):
739756
path = '/'
740757
method = 'GET'

tests/schemas/views.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from rest_framework.response import Response
1212
from rest_framework.schemas.openapi import AutoSchema
1313
from rest_framework.views import APIView
14-
from rest_framework.viewsets import GenericViewSet
14+
from rest_framework.viewsets import GenericViewSet, ViewSet
1515

1616

1717
class ExampleListView(APIView):
@@ -215,3 +215,25 @@ def get(self, *args, **kwargs):
215215

216216
serializer = self.get_serializer(data=now.date(), datetime=now)
217217
return Response(serializer.data)
218+
219+
220+
class ExampleViewSet(ViewSet):
221+
serializer_class = ExampleSerializerModel
222+
223+
def list(self, request):
224+
pass
225+
226+
def create(self, request):
227+
pass
228+
229+
def retrieve(self, request, pk=None):
230+
pass
231+
232+
def update(self, request, pk=None):
233+
pass
234+
235+
def partial_update(self, request, pk=None):
236+
pass
237+
238+
def destroy(self, request, pk=None):
239+
pass

0 commit comments

Comments
 (0)