Added OpenAPI tags to schemas. (#7184)

This commit is contained in:
Dhaval Mehta 2020-02-28 16:36:03 +05:30 committed by GitHub
parent e32ffbb12b
commit 2a5c2f3f70
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 146 additions and 0 deletions

View File

@ -215,6 +215,81 @@ This also applies to extra actions for `ViewSet`s:
If you wish to provide a base `AutoSchema` subclass to be used throughout your If you wish to provide a base `AutoSchema` subclass to be used throughout your
project you may adjust `settings.DEFAULT_SCHEMA_CLASS` appropriately. project you may adjust `settings.DEFAULT_SCHEMA_CLASS` appropriately.
### Grouping Operations With Tags
Tags can be used to group logical operations. Each tag name in the list MUST be unique.
---
#### Django REST Framework generates tags automatically with the following logic:
Tag name will be first element from the path. Also, any `_` in path name will be replaced by a `-`.
Consider below examples.
Example 1: Consider a user management system. The following table will illustrate the tag generation logic.
Here first element from the paths is: `users`. Hence tag wil be `users`
Http Method | Path | Tags
-------------------------------------|-------------------|-------------
PUT, PATCH, GET(Retrieve), DELETE | /users/{id}/ | ['users']
POST, GET(List) | /users/ | ['users']
Example 2: Consider a restaurant management system. The System has restaurants. Each restaurant has branches.
Consider REST APIs to deal with a branch of a particular restaurant.
Here first element from the paths is: `restaurants`. Hence tag wil be `restaurants`.
Http Method | Path | Tags
-------------------------------------|----------------------------------------------------|-------------------
PUT, PATCH, GET(Retrieve), DELETE: | /restaurants/{restaurant_id}/branches/{branch_id} | ['restaurants']
POST, GET(List): | /restaurants/{restaurant_id}/branches/ | ['restaurants']
Example 3: Consider Order items for an e commerce company.
Http Method | Path | Tags
-------------------------------------|-------------------------|-------------
PUT, PATCH, GET(Retrieve), DELETE | /order_items/{id}/ | ['order-items']
POST, GET(List) | /order_items/ | ['order-items']
---
#### Overriding auto generated tags:
You can override auto-generated tags by passing `tags` argument to the constructor of `AutoSchema`. `tags` argument must be a list or tuple of string.
```python
from rest_framework.schemas.openapi import AutoSchema
from rest_framework.views import APIView
class MyView(APIView):
schema = AutoSchema(tags=['tag1', 'tag2'])
...
```
If you need more customization, you can override the `get_tags` method of `AutoSchema` class. Consider the following example:
```python
from rest_framework.schemas.openapi import AutoSchema
from rest_framework.views import APIView
class MySchema(AutoSchema):
...
def get_tags(self, path, method):
if method == 'POST':
tags = ['tag1', 'tag2']
elif method == 'GET':
tags = ['tag2', 'tag3']
elif path == '/example/path/':
tags = ['tag3', 'tag4']
else:
tags = ['tag5', 'tag6', 'tag7']
return tags
class MyView(APIView):
schema = MySchema()
...
```
[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

View File

@ -71,6 +71,12 @@ class SchemaGenerator(BaseSchemaGenerator):
class AutoSchema(ViewInspector): class AutoSchema(ViewInspector):
def __init__(self, tags=None):
if tags and not all(isinstance(tag, str) for tag in tags):
raise ValueError('tags must be a list or tuple of string.')
self._tags = tags
super().__init__()
request_media_types = [] request_media_types = []
response_media_types = [] response_media_types = []
@ -98,6 +104,7 @@ class AutoSchema(ViewInspector):
if request_body: if request_body:
operation['requestBody'] = request_body operation['requestBody'] = request_body
operation['responses'] = self._get_responses(path, method) operation['responses'] = self._get_responses(path, method)
operation['tags'] = self.get_tags(path, method)
return operation return operation
@ -564,3 +571,16 @@ class AutoSchema(ViewInspector):
'description': "" 'description': ""
} }
} }
def get_tags(self, path, method):
# If user have specified tags, use them.
if self._tags:
return self._tags
# First element of a specific path could be valid tag. This is a fallback solution.
# PUT, PATCH, GET(Retrieve), DELETE: /user_profile/{id}/ tags = [user-profile]
# POST, GET(List): /user_profile/ tags = [user-profile]
if path.startswith('/'):
path = path[1:]
return [path.split('/')[0].replace('_', '-')]

View File

@ -126,6 +126,7 @@ class TestOperationIntrospection(TestCase):
'operationId': 'listDocStringExamples', 'operationId': 'listDocStringExamples',
'description': 'A description of my GET operation.', 'description': 'A description of my GET operation.',
'parameters': [], 'parameters': [],
'tags': ['example'],
'responses': { 'responses': {
'200': { '200': {
'description': '', 'description': '',
@ -166,6 +167,7 @@ class TestOperationIntrospection(TestCase):
'type': 'string', 'type': 'string',
}, },
}], }],
'tags': ['example'],
'responses': { 'responses': {
'200': { '200': {
'description': '', 'description': '',
@ -696,6 +698,55 @@ class TestOperationIntrospection(TestCase):
assert properties['ip']['type'] == 'string' assert properties['ip']['type'] == 'string'
assert 'format' not in properties['ip'] assert 'format' not in properties['ip']
def test_overridden_tags(self):
class ExampleStringTagsViewSet(views.ExampleGenericAPIView):
schema = AutoSchema(tags=['example1', 'example2'])
url_patterns = [
url(r'^test/?$', ExampleStringTagsViewSet.as_view()),
]
generator = SchemaGenerator(patterns=url_patterns)
schema = generator.get_schema(request=create_request('/'))
assert schema['paths']['/test/']['get']['tags'] == ['example1', 'example2']
def test_overridden_get_tags_method(self):
class MySchema(AutoSchema):
def get_tags(self, path, method):
if path.endswith('/new/'):
tags = ['tag1', 'tag2']
elif path.endswith('/old/'):
tags = ['tag2', 'tag3']
else:
tags = ['tag4', 'tag5']
return tags
class ExampleStringTagsViewSet(views.ExampleGenericViewSet):
schema = MySchema()
router = routers.SimpleRouter()
router.register('example', ExampleStringTagsViewSet, basename="example")
generator = SchemaGenerator(patterns=router.urls)
schema = generator.get_schema(request=create_request('/'))
assert schema['paths']['/example/new/']['get']['tags'] == ['tag1', 'tag2']
assert schema['paths']['/example/old/']['get']['tags'] == ['tag2', 'tag3']
def test_auto_generated_apiview_tags(self):
class RestaurantAPIView(views.ExampleGenericAPIView):
pass
class BranchAPIView(views.ExampleGenericAPIView):
pass
url_patterns = [
url(r'^any-dash_underscore/?$', RestaurantAPIView.as_view()),
url(r'^restaurants/branches/?$', BranchAPIView.as_view())
]
generator = SchemaGenerator(patterns=url_patterns)
schema = generator.get_schema(request=create_request('/'))
assert schema['paths']['/any-dash_underscore/']['get']['tags'] == ['any-dash-underscore']
assert schema['paths']['/restaurants/branches/']['get']['tags'] == ['restaurants']
@pytest.mark.skipif(uritemplate is None, reason='uritemplate not installed.') @pytest.mark.skipif(uritemplate is None, reason='uritemplate not installed.')
@override_settings(REST_FRAMEWORK={'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.openapi.AutoSchema'}) @override_settings(REST_FRAMEWORK={'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.openapi.AutoSchema'})