Skip to content

Commit

Permalink
Use an array type for list view response schemas
Browse files Browse the repository at this point in the history
This is the first part of encode#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.
  • Loading branch information
reupen committed Jul 30, 2019
1 parent 8b32f05 commit 337056e
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 40 deletions.
22 changes: 15 additions & 7 deletions rest_framework/schemas/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,22 +460,30 @@ def _get_request_body(self, path, method):
}

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
}
}
Expand Down
156 changes: 123 additions & 33 deletions tests/schemas/test_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,18 @@ def test_path_without_parameters(self):
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):
Expand Down Expand Up @@ -184,6 +195,83 @@ class View(generics.GenericAPIView):
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'
Expand Down Expand Up @@ -226,10 +314,11 @@ def test_serializer_datefield(self):
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 = '/'
Expand All @@ -243,45 +332,46 @@ def test_serializer_validators(self):
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.')
Expand Down

0 comments on commit 337056e

Please sign in to comment.