mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-22 09:36:49 +03:00
OpenAPI: Allow customizing operation name. (#7190)
This commit is contained in:
parent
94a09149b6
commit
5b16a17242
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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")
|
||||||
|
|
Loading…
Reference in New Issue
Block a user