Skip to content

Commit 05a51e9

Browse files
gnuletiksigvef
authored andcommitted
OpenAPI: Allow customizing operation name. (encode#7190)
1 parent c9e37ba commit 05a51e9

File tree

3 files changed

+129
-14
lines changed

3 files changed

+129
-14
lines changed

docs/api-guide/schemas.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,8 +288,41 @@ class MyView(APIView):
288288
...
289289
```
290290

291+
### OperationId
292+
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+
295+
If you have several views with the same model, the generator may generate duplicate operationId.
296+
In order to work around this, you can override the second part of the operationId: operation name.
297+
298+
```python
299+
from rest_framework.schemas.openapi import AutoSchema
300+
301+
class ExampleView(APIView):
302+
"""APIView subclass with custom schema introspection."""
303+
schema = AutoSchema(operation_id_base="Custom")
304+
```
305+
306+
The previous example will generate the following operationId: "ListCustoms", "RetrieveCustom", "UpdateCustom", "PartialUpdateCustom", "DestroyCustom".
307+
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.
308+
309+
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:
310+
311+
```python
312+
class CustomSchema(AutoSchema):
313+
def get_operation_id_base(self, path, method, action):
314+
pass
315+
316+
def get_operation_id(self, path, method):
317+
pass
318+
319+
class CustomView(APIView):
320+
"""APIView subclass with custom schema introspection."""
321+
schema = CustomSchema()
322+
```
291323

292324
[openapi]: https://github.com/OAI/OpenAPI-Specification
293325
[openapi-specification-extensions]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#specification-extensions
294326
[openapi-operation]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operationObject
295327
[openapi-tags]: https://swagger.io/specification/#tagObject
328+
[openapi-operationid]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#fixed-fields-17

rest_framework/schemas/openapi.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,14 @@ def get_schema(self, request=None, public=False):
7171

7272
class AutoSchema(ViewInspector):
7373

74-
def __init__(self, tags=None):
74+
def __init__(self, operation_id_base=None, tags=None):
75+
"""
76+
:param operation_id_base: user-defined name in operationId. If empty, it will be deducted from the Model/Serializer/View name.
77+
"""
7578
if tags and not all(isinstance(tag, str) for tag in tags):
7679
raise ValueError('tags must be a list or tuple of string.')
7780
self._tags = tags
81+
self.operation_id_base = operation_id_base
7882
super().__init__()
7983

8084
request_media_types = []
@@ -91,7 +95,7 @@ def __init__(self, tags=None):
9195
def get_operation(self, path, method):
9296
operation = {}
9397

94-
operation['operationId'] = self._get_operation_id(path, method)
98+
operation['operationId'] = self.get_operation_id(path, method)
9599
operation['description'] = self.get_description(path, method)
96100

97101
parameters = []
@@ -108,21 +112,17 @@ def get_operation(self, path, method):
108112

109113
return operation
110114

111-
def _get_operation_id(self, path, method):
115+
def get_operation_id_base(self, path, method, action):
112116
"""
113-
Compute an operation ID from the model, serializer or view name.
117+
Compute the base part for operation ID from the model, serializer or view name.
114118
"""
115-
method_name = getattr(self.view, 'action', method.lower())
116-
if is_list_view(path, method, self.view):
117-
action = 'list'
118-
elif method_name not in self.method_mapping:
119-
action = method_name
120-
else:
121-
action = self.method_mapping[method.lower()]
119+
model = getattr(getattr(self.view, 'queryset', None), 'model', None)
120+
121+
if self.operation_id_base is not None:
122+
name = self.operation_id_base
122123

123124
# Try to deduce the ID from the view's model
124-
model = getattr(getattr(self.view, 'queryset', None), 'model', None)
125-
if model is not None:
125+
elif model is not None:
126126
name = model.__name__
127127

128128
# Try with the serializer class name
@@ -147,6 +147,22 @@ def _get_operation_id(self, path, method):
147147
if action == 'list' and not name.endswith('s'): # listThings instead of listThing
148148
name += 's'
149149

150+
return name
151+
152+
def get_operation_id(self, path, method):
153+
"""
154+
Compute an operation ID from the view type and get_operation_id_base method.
155+
"""
156+
method_name = getattr(self.view, 'action', method.lower())
157+
if is_list_view(path, method, self.view):
158+
action = 'list'
159+
elif method_name not in self.method_mapping:
160+
action = method_name
161+
else:
162+
action = self.method_mapping[method.lower()]
163+
164+
name = self.get_operation_id_base(path, method, action)
165+
150166
return action + name
151167

152168
def _get_path_parameters(self, path, method):

tests/schemas/test_openapi.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,9 +575,75 @@ def test_operation_id_generation(self):
575575
inspector = AutoSchema()
576576
inspector.view = view
577577

578-
operationId = inspector._get_operation_id(path, method)
578+
operationId = inspector.get_operation_id(path, method)
579579
assert operationId == 'listExamples'
580580

581+
def test_operation_id_custom_operation_id_base(self):
582+
path = '/'
583+
method = 'GET'
584+
585+
view = create_view(
586+
views.ExampleGenericAPIView,
587+
method,
588+
create_request(path),
589+
)
590+
inspector = AutoSchema(operation_id_base="Ulysse")
591+
inspector.view = view
592+
593+
operationId = inspector.get_operation_id(path, method)
594+
assert operationId == 'listUlysses'
595+
596+
def test_operation_id_custom_name(self):
597+
path = '/'
598+
method = 'GET'
599+
600+
view = create_view(
601+
views.ExampleGenericAPIView,
602+
method,
603+
create_request(path),
604+
)
605+
inspector = AutoSchema(operation_id_base='Ulysse')
606+
inspector.view = view
607+
608+
operationId = inspector.get_operation_id(path, method)
609+
assert operationId == 'listUlysses'
610+
611+
def test_operation_id_override_get(self):
612+
class CustomSchema(AutoSchema):
613+
def get_operation_id(self, path, method):
614+
return 'myCustomOperationId'
615+
616+
path = '/'
617+
method = 'GET'
618+
view = create_view(
619+
views.ExampleGenericAPIView,
620+
method,
621+
create_request(path),
622+
)
623+
inspector = CustomSchema()
624+
inspector.view = view
625+
626+
operationId = inspector.get_operation_id(path, method)
627+
assert operationId == 'myCustomOperationId'
628+
629+
def test_operation_id_override_base(self):
630+
class CustomSchema(AutoSchema):
631+
def get_operation_id_base(self, path, method, action):
632+
return 'Item'
633+
634+
path = '/'
635+
method = 'GET'
636+
view = create_view(
637+
views.ExampleGenericAPIView,
638+
method,
639+
create_request(path),
640+
)
641+
inspector = CustomSchema()
642+
inspector.view = view
643+
644+
operationId = inspector.get_operation_id(path, method)
645+
assert operationId == 'listItem'
646+
581647
def test_repeat_operation_ids(self):
582648
router = routers.SimpleRouter()
583649
router.register('account', views.ExampleGenericViewSet, basename="account")

0 commit comments

Comments
 (0)