mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-25 19:14:01 +03:00
Added OpenAPI tags to schemas. (#7184)
This commit is contained in:
parent
e32ffbb12b
commit
2a5c2f3f70
|
@ -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
|
||||||
|
|
|
@ -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('_', '-')]
|
||||||
|
|
|
@ -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'})
|
||||||
|
|
Loading…
Reference in New Issue
Block a user