From 558dd0e19e152c00bbd8069ea83b7e70c09c28d7 Mon Sep 17 00:00:00 2001 From: Jonatan Zint Date: Wed, 29 Apr 2020 15:54:45 +0200 Subject: [PATCH] Always wrap response in a component schema, also paginated once, to improve processability of API code generators #7299 --- rest_framework/schemas/openapi.py | 61 ++++++++++++++++++++++--------- tests/schemas/test_openapi.py | 38 ++++++++++++------- 2 files changed, 67 insertions(+), 32 deletions(-) diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index 9b3082822..f6e9f22c4 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -189,6 +189,8 @@ class AutoSchema(ViewInspector): Return components with their properties from the serializer. """ + components = {} + if method.lower() == 'delete': return {} @@ -197,10 +199,28 @@ class AutoSchema(ViewInspector): if not isinstance(serializer, serializers.Serializer): return {} - component_name = self.get_component_name(serializer) + item_component_name = self.get_component_name(serializer) + item_schema = self.map_serializer(serializer) + components[item_component_name] = item_schema - content = self.map_serializer(serializer) - return {component_name: content} + response_component_name = self._get_response_component_name( + self.get_operation_id(path, method) + ) + + if is_list_view(path, method, self.view): + response_component_schema = { + 'type': 'array', + 'items': self._get_serializer_reference(serializer), + } + paginator = self.get_paginator() + if paginator: + response_component_schema = paginator.get_paginated_response_schema(response_component_schema) + else: + response_component_schema = self._get_serializer_reference(serializer) + + components[response_component_name] = response_component_schema + + return components def _to_camel_case(self, snake_str): components = snake_str.split('_') @@ -613,9 +633,17 @@ class AutoSchema(ViewInspector): .format(view.__class__.__name__, method, path)) return None - def _get_reference(self, serializer): + def _get_serializer_reference(self, serializer): return {'$ref': '#/components/schemas/{}'.format(self.get_component_name(serializer))} + @staticmethod + def _get_response_component_name(operation_id): + operation_id = operation_id[0].upper() + operation_id[1:] + return operation_id + 'Response' + + def _get_response_reference(self, operation_id): + return {'$ref': '#/components/schemas/{0}'.format(self._get_response_component_name(operation_id))} + def get_request_body(self, path, method): if method not in ('PUT', 'PATCH', 'POST'): return {} @@ -627,7 +655,7 @@ class AutoSchema(ViewInspector): if not isinstance(serializer, serializers.Serializer): item_schema = {} else: - item_schema = self._get_reference(serializer) + item_schema = self._get_serializer_reference(serializer) return { 'content': { @@ -649,20 +677,17 @@ class AutoSchema(ViewInspector): serializer = self.get_serializer(path, method) if not isinstance(serializer, serializers.Serializer): - item_schema = {} + if is_list_view(path, method, self.view): + response_schema = { + 'type': 'array', + 'items': {} + } + else: + response_schema = {} else: - item_schema = self._get_reference(serializer) - - if is_list_view(path, method, self.view): - response_schema = { - 'type': 'array', - 'items': item_schema, - } - paginator = self.get_paginator() - if paginator: - response_schema = paginator.get_paginated_response_schema(response_schema) - else: - response_schema = item_schema + response_schema = self._get_response_reference( + self.get_operation_id(path, method) + ) status_code = '201' if method == 'POST' else '200' return { status_code: { diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 0e86a7f50..50855a13e 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -308,7 +308,8 @@ class TestOperationIntrospection(TestCase): inspector.view = view responses = inspector.get_responses(path, method) - assert responses['201']['content']['application/json']['schema']['$ref'] == '#/components/schemas/Item' + assert responses['201']['content']['application/json']['schema']['$ref'] == \ + '#/components/schemas/CreateItemResponse' components = inspector.get_components(path, method) assert sorted(components['Item']['required']) == ['text', 'write_only'] @@ -338,7 +339,7 @@ class TestOperationIntrospection(TestCase): inspector.view = view responses = inspector.get_responses(path, method) - assert responses['201']['content']['application/json']['schema']['$ref'] == '#/components/schemas/Item' + assert responses['201']['content']['application/json']['schema']['$ref'] == '#/components/schemas/CreateItemResponse' components = inspector.get_components(path, method) assert components['Item'] @@ -375,10 +376,7 @@ class TestOperationIntrospection(TestCase): 'content': { 'application/json': { 'schema': { - 'type': 'array', - 'items': { - '$ref': '#/components/schemas/Item' - }, + '$ref': '#/components/schemas/ListItemsResponse' }, }, }, @@ -386,6 +384,12 @@ class TestOperationIntrospection(TestCase): } components = inspector.get_components(path, method) assert components == { + 'ListItemsResponse': { + 'type': 'array', + 'items': { + '$ref': '#/components/schemas/Item', + }, + }, 'Item': { 'type': 'object', 'properties': { @@ -431,13 +435,7 @@ class TestOperationIntrospection(TestCase): 'content': { 'application/json': { 'schema': { - 'type': 'object', - 'item': { - 'type': 'array', - 'items': { - '$ref': '#/components/schemas/Item' - }, - }, + '$ref': '#/components/schemas/ListItemsResponse' }, }, }, @@ -445,6 +443,15 @@ class TestOperationIntrospection(TestCase): } components = inspector.get_components(path, method) assert components == { + 'ListItemsResponse': { + 'type': 'object', + 'item': { + 'type': 'array', + 'items': { + '$ref': '#/components/schemas/Item', + }, + }, + }, 'Item': { 'type': 'object', 'properties': { @@ -601,7 +608,7 @@ class TestOperationIntrospection(TestCase): 'content': { 'application/json': { 'schema': { - '$ref': '#/components/schemas/Item' + '$ref': '#/components/schemas/RetrieveItemResponse' }, }, }, @@ -610,6 +617,9 @@ class TestOperationIntrospection(TestCase): components = inspector.get_components(path, method) assert components == { + 'RetrieveItemResponse': { + '$ref': '#/components/schemas/Item' + }, 'Item': { 'type': 'object', 'properties': {