OpenAPI: Allow customizing operation name. (#7190)

This commit is contained in:
Martin Desrumaux 2020-03-02 16:40:18 +01:00 committed by GitHub
parent 94a09149b6
commit 5b16a17242
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 129 additions and 14 deletions

View File

@ -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]: 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-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-operation]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operationObject
[openapi-tags]: https://swagger.io/specification/#tagObject [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

View File

@ -71,10 +71,14 @@ class SchemaGenerator(BaseSchemaGenerator):
class AutoSchema(ViewInspector): 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): if tags and not all(isinstance(tag, str) for tag in tags):
raise ValueError('tags must be a list or tuple of string.') raise ValueError('tags must be a list or tuple of string.')
self._tags = tags self._tags = tags
self.operation_id_base = operation_id_base
super().__init__() super().__init__()
request_media_types = [] request_media_types = []
@ -91,7 +95,7 @@ class AutoSchema(ViewInspector):
def get_operation(self, path, method): def get_operation(self, path, method):
operation = {} operation = {}
operation['operationId'] = self._get_operation_id(path, method) operation['operationId'] = self.get_operation_id(path, method)
operation['description'] = self.get_description(path, method) operation['description'] = self.get_description(path, method)
parameters = [] parameters = []
@ -108,21 +112,17 @@ class AutoSchema(ViewInspector):
return operation 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()) model = getattr(getattr(self.view, 'queryset', None), 'model', None)
if is_list_view(path, method, self.view):
action = 'list' if self.operation_id_base is not None:
elif method_name not in self.method_mapping: name = self.operation_id_base
action = method_name
else:
action = self.method_mapping[method.lower()]
# Try to deduce the ID from the view's model # Try to deduce the ID from the view's model
model = getattr(getattr(self.view, 'queryset', None), 'model', None) elif model is not None:
if model is not None:
name = model.__name__ name = model.__name__
# Try with the serializer class name # Try with the serializer class name
@ -147,6 +147,22 @@ class AutoSchema(ViewInspector):
if action == 'list' and not name.endswith('s'): # listThings instead of listThing if action == 'list' and not name.endswith('s'): # listThings instead of listThing
name += 's' 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 return action + name
def _get_path_parameters(self, path, method): def _get_path_parameters(self, path, method):

View File

@ -575,9 +575,75 @@ class TestOperationIntrospection(TestCase):
inspector = AutoSchema() inspector = AutoSchema()
inspector.view = view inspector.view = view
operationId = inspector._get_operation_id(path, method) operationId = inspector.get_operation_id(path, method)
assert operationId == 'listExamples' 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): def test_repeat_operation_ids(self):
router = routers.SimpleRouter() router = routers.SimpleRouter()
router.register('account', views.ExampleGenericViewSet, basename="account") router.register('account', views.ExampleGenericViewSet, basename="account")