mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-22 09:36:49 +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
|
||||
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-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-tags]: https://swagger.io/specification/#tagObject
|
||||
|
|
|
@ -71,6 +71,12 @@ class SchemaGenerator(BaseSchemaGenerator):
|
|||
|
||||
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 = []
|
||||
response_media_types = []
|
||||
|
||||
|
@ -98,6 +104,7 @@ class AutoSchema(ViewInspector):
|
|||
if request_body:
|
||||
operation['requestBody'] = request_body
|
||||
operation['responses'] = self._get_responses(path, method)
|
||||
operation['tags'] = self.get_tags(path, method)
|
||||
|
||||
return operation
|
||||
|
||||
|
@ -564,3 +571,16 @@ class AutoSchema(ViewInspector):
|
|||
'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',
|
||||
'description': 'A description of my GET operation.',
|
||||
'parameters': [],
|
||||
'tags': ['example'],
|
||||
'responses': {
|
||||
'200': {
|
||||
'description': '',
|
||||
|
@ -166,6 +167,7 @@ class TestOperationIntrospection(TestCase):
|
|||
'type': 'string',
|
||||
},
|
||||
}],
|
||||
'tags': ['example'],
|
||||
'responses': {
|
||||
'200': {
|
||||
'description': '',
|
||||
|
@ -696,6 +698,55 @@ class TestOperationIntrospection(TestCase):
|
|||
assert properties['ip']['type'] == 'string'
|
||||
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.')
|
||||
@override_settings(REST_FRAMEWORK={'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.openapi.AutoSchema'})
|
||||
|
|
Loading…
Reference in New Issue
Block a user