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.
This commit is contained in:
Reupen Shah 2019-07-30 21:19:43 +01:00 committed by Carlton Gibson
parent a3f244d85e
commit b45ff07294
2 changed files with 138 additions and 40 deletions

View File

@ -460,22 +460,30 @@ class AutoSchema(ViewInspector):
} }
def _get_responses(self, path, method): def _get_responses(self, path, method):
# TODO: Handle multiple codes. # TODO: Handle multiple codes and pagination classes.
content = {} item_schema = {}
serializer = self._get_serializer(path, method) serializer = self._get_serializer(path, method)
if isinstance(serializer, serializers.Serializer): if isinstance(serializer, serializers.Serializer):
content = self._map_serializer(serializer) item_schema = self._map_serializer(serializer)
# No write_only fields for response. # 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: if 'writeOnly' in schema:
del content['properties'][name] del item_schema['properties'][name]
content['required'] = [f for f in content['required'] if f != 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 { return {
'200': { '200': {
'content': { 'content': {
ct: {'schema': content} ct: {'schema': response_schema}
for ct in self.content_types for ct in self.content_types
} }
} }

View File

@ -82,7 +82,18 @@ class TestOperationIntrospection(TestCase):
assert operation == { assert operation == {
'operationId': 'ListExamples', 'operationId': 'ListExamples',
'parameters': [], 'parameters': [],
'responses': {'200': {'content': {'application/json': {'schema': {}}}}}, 'responses': {
'200': {
'content': {
'application/json': {
'schema': {
'type': 'array',
'items': {},
},
},
},
},
},
} }
def test_path_with_id_parameter(self): def test_path_with_id_parameter(self):
@ -184,6 +195,83 @@ class TestOperationIntrospection(TestCase):
assert list(schema['properties']['nested']['properties'].keys()) == ['number'] assert list(schema['properties']['nested']['properties'].keys()) == ['number']
assert schema['properties']['nested']['required'] == ['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): def test_operation_id_generation(self):
path = '/' path = '/'
method = 'GET' method = 'GET'
@ -226,10 +314,11 @@ class TestOperationIntrospection(TestCase):
inspector.view = view inspector.view = view
responses = inspector._get_responses(path, method) responses = inspector._get_responses(path, method)
response_schema = responses['200']['content']['application/json']['schema']['properties'] response_schema = responses['200']['content']['application/json']['schema']
assert response_schema['date']['type'] == response_schema['datetime']['type'] == 'string' properties = response_schema['items']['properties']
assert response_schema['date']['format'] == 'date' assert properties['date']['type'] == properties['datetime']['type'] == 'string'
assert response_schema['datetime']['format'] == 'date-time' assert properties['date']['format'] == 'date'
assert properties['datetime']['format'] == 'date-time'
def test_serializer_validators(self): def test_serializer_validators(self):
path = '/' path = '/'
@ -243,45 +332,46 @@ class TestOperationIntrospection(TestCase):
inspector.view = view inspector.view = view
responses = inspector._get_responses(path, method) 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 properties['integer']['type'] == 'integer'
assert response_schema['integer']['maximum'] == 99 assert properties['integer']['maximum'] == 99
assert response_schema['integer']['minimum'] == -11 assert properties['integer']['minimum'] == -11
assert response_schema['string']['minLength'] == 2 assert properties['string']['minLength'] == 2
assert response_schema['string']['maxLength'] == 10 assert properties['string']['maxLength'] == 10
assert response_schema['regex']['pattern'] == r'[ABC]12{3}' assert properties['regex']['pattern'] == r'[ABC]12{3}'
assert response_schema['regex']['description'] == 'must have an A, B, or C followed by 1222' assert properties['regex']['description'] == 'must have an A, B, or C followed by 1222'
assert response_schema['decimal1']['type'] == 'number' assert properties['decimal1']['type'] == 'number'
assert response_schema['decimal1']['multipleOf'] == .01 assert properties['decimal1']['multipleOf'] == .01
assert response_schema['decimal1']['maximum'] == 10000 assert properties['decimal1']['maximum'] == 10000
assert response_schema['decimal1']['minimum'] == -10000 assert properties['decimal1']['minimum'] == -10000
assert response_schema['decimal2']['type'] == 'number' assert properties['decimal2']['type'] == 'number'
assert response_schema['decimal2']['multipleOf'] == .0001 assert properties['decimal2']['multipleOf'] == .0001
assert response_schema['email']['type'] == 'string' assert properties['email']['type'] == 'string'
assert response_schema['email']['format'] == 'email' assert properties['email']['format'] == 'email'
assert response_schema['email']['default'] == 'foo@bar.com' assert properties['email']['default'] == 'foo@bar.com'
assert response_schema['url']['type'] == 'string' assert properties['url']['type'] == 'string'
assert response_schema['url']['nullable'] is True assert properties['url']['nullable'] is True
assert response_schema['url']['default'] == 'http://www.example.com' assert properties['url']['default'] == 'http://www.example.com'
assert response_schema['uuid']['type'] == 'string' assert properties['uuid']['type'] == 'string'
assert response_schema['uuid']['format'] == 'uuid' assert properties['uuid']['format'] == 'uuid'
assert response_schema['ip4']['type'] == 'string' assert properties['ip4']['type'] == 'string'
assert response_schema['ip4']['format'] == 'ipv4' assert properties['ip4']['format'] == 'ipv4'
assert response_schema['ip6']['type'] == 'string' assert properties['ip6']['type'] == 'string'
assert response_schema['ip6']['format'] == 'ipv6' assert properties['ip6']['format'] == 'ipv6'
assert response_schema['ip']['type'] == 'string' assert properties['ip']['type'] == 'string'
assert 'format' not in response_schema['ip'] assert 'format' not in properties['ip']
@pytest.mark.skipif(uritemplate is None, reason='uritemplate not installed.') @pytest.mark.skipif(uritemplate is None, reason='uritemplate not installed.')