From 337056ed05ea4d0918ac476fc12dd7aabfbc1b4b Mon Sep 17 00:00:00 2001 From: Reupen Shah Date: Tue, 30 Jul 2019 21:19:43 +0100 Subject: [PATCH] Use an array type for list view response schemas This is the first part of #6846. Previously, the response schema for list views was an object representing a single item. However, list views return a list of items, and hence it should be an array. Further work will need to be done to support how pagination classes modify list responses. There should be no change for views not determined to be list views. --- rest_framework/schemas/openapi.py | 22 +++-- tests/schemas/test_openapi.py | 156 +++++++++++++++++++++++------- 2 files changed, 138 insertions(+), 40 deletions(-) diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index 32076eaa6..ac828935f 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -460,22 +460,30 @@ class AutoSchema(ViewInspector): } def _get_responses(self, path, method): - # TODO: Handle multiple codes. - content = {} + # TODO: Handle multiple codes and pagination classes. + item_schema = {} serializer = self._get_serializer(path, method) if isinstance(serializer, serializers.Serializer): - content = self._map_serializer(serializer) + item_schema = self._map_serializer(serializer) # No write_only fields for response. - for name, schema in content['properties'].copy().items(): + for name, schema in item_schema['properties'].copy().items(): if 'writeOnly' in schema: - del content['properties'][name] - content['required'] = [f for f in content['required'] if f != name] + del item_schema['properties'][name] + item_schema['required'] = [f for f in item_schema['required'] if f != name] + + if is_list_view(path, method, self.view): + response_schema = { + 'type': 'array', + 'items': item_schema, + } + else: + response_schema = item_schema return { '200': { 'content': { - ct: {'schema': content} + ct: {'schema': response_schema} for ct in self.content_types } } diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index b5727400b..993062107 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -82,7 +82,18 @@ class TestOperationIntrospection(TestCase): assert operation == { 'operationId': 'ListExamples', 'parameters': [], - 'responses': {'200': {'content': {'application/json': {'schema': {}}}}}, + 'responses': { + '200': { + 'content': { + 'application/json': { + 'schema': { + 'type': 'array', + 'items': {}, + }, + }, + }, + }, + }, } def test_path_with_id_parameter(self): @@ -184,6 +195,83 @@ class TestOperationIntrospection(TestCase): assert list(schema['properties']['nested']['properties'].keys()) == ['number'] assert schema['properties']['nested']['required'] == ['number'] + def test_list_response_body_generation(self): + """Test that an array schema is returned for list views.""" + path = '/' + method = 'GET' + + class ItemSerializer(serializers.Serializer): + text = serializers.CharField() + + class View(generics.GenericAPIView): + serializer_class = ItemSerializer + + view = create_view( + View, + method, + create_request(path), + ) + inspector = AutoSchema() + inspector.view = view + + responses = inspector._get_responses(path, method) + assert responses == { + '200': { + 'content': { + 'application/json': { + 'schema': { + 'type': 'array', + 'items': { + 'properties': { + 'text': { + 'type': 'string', + }, + }, + 'required': ['text'], + }, + }, + }, + }, + }, + } + + def test_retrieve_response_body_generation(self): + """Test that a list of properties is returned for retrieve item views.""" + path = '/{id}/' + method = 'GET' + + class ItemSerializer(serializers.Serializer): + text = serializers.CharField() + + class View(generics.GenericAPIView): + serializer_class = ItemSerializer + + view = create_view( + View, + method, + create_request(path), + ) + inspector = AutoSchema() + inspector.view = view + + responses = inspector._get_responses(path, method) + assert responses == { + '200': { + 'content': { + 'application/json': { + 'schema': { + 'properties': { + 'text': { + 'type': 'string', + }, + }, + 'required': ['text'], + }, + }, + }, + }, + } + def test_operation_id_generation(self): path = '/' method = 'GET' @@ -226,10 +314,11 @@ class TestOperationIntrospection(TestCase): inspector.view = view responses = inspector._get_responses(path, method) - response_schema = responses['200']['content']['application/json']['schema']['properties'] - assert response_schema['date']['type'] == response_schema['datetime']['type'] == 'string' - assert response_schema['date']['format'] == 'date' - assert response_schema['datetime']['format'] == 'date-time' + response_schema = responses['200']['content']['application/json']['schema'] + properties = response_schema['items']['properties'] + assert properties['date']['type'] == properties['datetime']['type'] == 'string' + assert properties['date']['format'] == 'date' + assert properties['datetime']['format'] == 'date-time' def test_serializer_validators(self): path = '/' @@ -243,45 +332,46 @@ class TestOperationIntrospection(TestCase): inspector.view = view responses = inspector._get_responses(path, method) - response_schema = responses['200']['content']['application/json']['schema']['properties'] + response_schema = responses['200']['content']['application/json']['schema'] + properties = response_schema['items']['properties'] - assert response_schema['integer']['type'] == 'integer' - assert response_schema['integer']['maximum'] == 99 - assert response_schema['integer']['minimum'] == -11 + assert properties['integer']['type'] == 'integer' + assert properties['integer']['maximum'] == 99 + assert properties['integer']['minimum'] == -11 - assert response_schema['string']['minLength'] == 2 - assert response_schema['string']['maxLength'] == 10 + assert properties['string']['minLength'] == 2 + assert properties['string']['maxLength'] == 10 - assert response_schema['regex']['pattern'] == r'[ABC]12{3}' - assert response_schema['regex']['description'] == 'must have an A, B, or C followed by 1222' + assert properties['regex']['pattern'] == r'[ABC]12{3}' + assert properties['regex']['description'] == 'must have an A, B, or C followed by 1222' - assert response_schema['decimal1']['type'] == 'number' - assert response_schema['decimal1']['multipleOf'] == .01 - assert response_schema['decimal1']['maximum'] == 10000 - assert response_schema['decimal1']['minimum'] == -10000 + assert properties['decimal1']['type'] == 'number' + assert properties['decimal1']['multipleOf'] == .01 + assert properties['decimal1']['maximum'] == 10000 + assert properties['decimal1']['minimum'] == -10000 - assert response_schema['decimal2']['type'] == 'number' - assert response_schema['decimal2']['multipleOf'] == .0001 + assert properties['decimal2']['type'] == 'number' + assert properties['decimal2']['multipleOf'] == .0001 - assert response_schema['email']['type'] == 'string' - assert response_schema['email']['format'] == 'email' - assert response_schema['email']['default'] == 'foo@bar.com' + assert properties['email']['type'] == 'string' + assert properties['email']['format'] == 'email' + assert properties['email']['default'] == 'foo@bar.com' - assert response_schema['url']['type'] == 'string' - assert response_schema['url']['nullable'] is True - assert response_schema['url']['default'] == 'http://www.example.com' + assert properties['url']['type'] == 'string' + assert properties['url']['nullable'] is True + assert properties['url']['default'] == 'http://www.example.com' - assert response_schema['uuid']['type'] == 'string' - assert response_schema['uuid']['format'] == 'uuid' + assert properties['uuid']['type'] == 'string' + assert properties['uuid']['format'] == 'uuid' - assert response_schema['ip4']['type'] == 'string' - assert response_schema['ip4']['format'] == 'ipv4' + assert properties['ip4']['type'] == 'string' + assert properties['ip4']['format'] == 'ipv4' - assert response_schema['ip6']['type'] == 'string' - assert response_schema['ip6']['format'] == 'ipv6' + assert properties['ip6']['type'] == 'string' + assert properties['ip6']['format'] == 'ipv6' - assert response_schema['ip']['type'] == 'string' - assert 'format' not in response_schema['ip'] + assert properties['ip']['type'] == 'string' + assert 'format' not in properties['ip'] @pytest.mark.skipif(uritemplate is None, reason='uritemplate not installed.')