Skip to content

Possible solution for tags generation #7184

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Feb 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d4e9b6e
add tag generation logic
dhaval-mehta Feb 6, 2020
bb339f4
FIX existing test cases
dhaval-mehta Feb 6, 2020
44c1c25
add support for tag objects
dhaval-mehta Feb 9, 2020
a5eec91
improve tag generation from viewset name
dhaval-mehta Feb 9, 2020
ac145a4
add documentation for tags
dhaval-mehta Feb 9, 2020
1baeb24
fix grammatical error
dhaval-mehta Feb 9, 2020
8744470
remove extra line
dhaval-mehta Feb 9, 2020
25f1425
remove APIView name check
dhaval-mehta Feb 9, 2020
05d8a7b
add ExampleTagsViewSet view
dhaval-mehta Feb 9, 2020
4b4f1c1
add test cases for tag generation
dhaval-mehta Feb 9, 2020
9c3a632
minor improvement in documentation
dhaval-mehta Feb 9, 2020
b0f11cd
fix changes given by kevin-brown
dhaval-mehta Feb 12, 2020
10cdd2b
improve documentation
dhaval-mehta Feb 12, 2020
ee97de3
improve documentation
dhaval-mehta Feb 12, 2020
31a1eb1
add test case for tag generation from view-set
dhaval-mehta Feb 12, 2020
56178ed
remove support for dict tags
dhaval-mehta Feb 18, 2020
912f22a
change tag name style to url path style
dhaval-mehta Feb 18, 2020
cc2a8a5
remove test cases for tag objects
dhaval-mehta Feb 18, 2020
8d3051d
add better example in comments
dhaval-mehta Feb 19, 2020
4229234
sync documentation with implementation.
dhaval-mehta Feb 19, 2020
d77afd5
improve documentation
dhaval-mehta Feb 19, 2020
22da477
change _get_tags to get_tags
dhaval-mehta Feb 19, 2020
95831b5
add guidance for overriding get_tags method
dhaval-mehta Feb 19, 2020
f438f14
add test case for method override use case
dhaval-mehta Feb 19, 2020
48c02dd
improve error message
dhaval-mehta Feb 19, 2020
64a4828
remove tag generation from viewset
dhaval-mehta Feb 20, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions docs/api-guide/schemas.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
20 changes: 20 additions & 0 deletions rest_framework/schemas/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ def get_schema(self, request=None, public=False):

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 = []

Expand Down Expand Up @@ -98,6 +104,7 @@ def get_operation(self, path, method):
if request_body:
operation['requestBody'] = request_body
operation['responses'] = self._get_responses(path, method)
operation['tags'] = self.get_tags(path, method)

return operation

Expand Down Expand Up @@ -564,3 +571,16 @@ def _get_responses(self, path, method):
'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('_', '-')]
51 changes: 51 additions & 0 deletions tests/schemas/test_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ def test_path_without_parameters(self):
'operationId': 'listDocStringExamples',
'description': 'A description of my GET operation.',
'parameters': [],
'tags': ['example'],
'responses': {
'200': {
'description': '',
Expand Down Expand Up @@ -166,6 +167,7 @@ def test_path_with_id_parameter(self):
'type': 'string',
},
}],
'tags': ['example'],
'responses': {
'200': {
'description': '',
Expand Down Expand Up @@ -696,6 +698,55 @@ def test_serializer_validators(self):
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'})
Expand Down