Skip to content

Commit ddc2aee

Browse files
authored
Merge pull request #663 from flayman/661-api-response-code-as-string
Fix for issue #661
2 parents fe9fec6 + 10b8a65 commit ddc2aee

File tree

5 files changed

+59
-4
lines changed

5 files changed

+59
-4
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Current
1717
- Ensure `basePath` is always a path
1818
- Hide Namespaces with all hidden Resources from Swagger documentation
1919
- Per route Swagger documentation for multiple routes on a ``Resource``
20+
- Fix Swagger `duplicate mapping key` problem from conflicts between response codes given as string or integer (:issue`661`)
2021

2122
0.12.1 (2018-09-28)
2223
-------------------

flask_restplus/namespace.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ def marshal_with(self, fields, as_list=False, code=HTTPStatus.OK, description=No
238238
def wrapper(func):
239239
doc = {
240240
'responses': {
241-
code: (description, [fields]) if as_list else (description, fields)
241+
str(code): (description, [fields]) if as_list else (description, fields)
242242
},
243243
'__mask__': kwargs.get('mask', True), # Mask values can't be determined outside app context
244244
}
@@ -289,7 +289,7 @@ def response(self, code, description, model=None, **kwargs):
289289
:param ModelBase model: an optional response model
290290
291291
'''
292-
return self.doc(responses={code: (description, model, kwargs)})
292+
return self.doc(responses={str(code): (description, model, kwargs)})
293293

294294
def header(self, name, description=None, **kwargs):
295295
'''

flask_restplus/swagger.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,7 @@ def responses_for(self, doc, method):
485485
for d in doc, doc[method]:
486486
if 'responses' in d:
487487
for code, response in iteritems(d['responses']):
488+
code = str(code)
488489
if isinstance(response, string_types):
489490
description = response
490491
model = None
@@ -514,7 +515,7 @@ def responses_for(self, doc, method):
514515
for name, description in iteritems(d['docstring']['raises']):
515516
for exception, handler in iteritems(self.api.error_handlers):
516517
error_responses = getattr(handler, '__apidoc__', {}).get('responses', {})
517-
code = list(error_responses.keys())[0] if error_responses else None
518+
code = str(list(error_responses.keys())[0]) if error_responses else None
518519
if code and exception.__name__ == name:
519520
responses[code] = {'$ref': '#/responses/{0}'.format(name)}
520521
break

tests/conftest.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,23 @@
1111

1212

1313
class TestClient(FlaskClient):
14+
# Borrowed from https://pythonadventures.wordpress.com/2016/03/06/detect-duplicate-keys-in-a-json-file/
15+
# Thank you to Wordpress author @ubuntuincident, aka Jabba Laci.
16+
def dict_raise_on_duplicates(self, ordered_pairs):
17+
"""Reject duplicate keys."""
18+
d = {}
19+
for k, v in ordered_pairs:
20+
if k in d:
21+
raise ValueError("duplicate key: %r" % (k,))
22+
else:
23+
d[k] = v
24+
return d
25+
1426
def get_json(self, url, status=200, **kwargs):
1527
response = self.get(url, **kwargs)
1628
assert response.status_code == status
1729
assert response.content_type == 'application/json'
18-
return json.loads(response.data.decode('utf8'))
30+
return json.loads(response.data.decode('utf8'), object_pairs_hook=self.dict_raise_on_duplicates)
1931

2032
def post_json(self, url, data, status=200, **kwargs):
2133
response = self.post(url, data=json.dumps(data),

tests/test_swagger.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2042,6 +2042,47 @@ def get(self):
20422042

20432043
client.get_specs(status=500)
20442044

2045+
def test_specs_no_duplicate_response_keys(self, api, client):
2046+
'''
2047+
This tests that the swagger.json document will not be written with duplicate object keys
2048+
due to the coercion of dict keys to string. The last @api.response should win.
2049+
'''
2050+
# Note the use of a strings '404' and '200' in class decorators as opposed to ints in method decorators.
2051+
@api.response('404', 'Not Found')
2052+
class BaseResource(restplus.Resource):
2053+
def get(self):
2054+
pass
2055+
2056+
model = api.model('SomeModel', {
2057+
'message': restplus.fields.String,
2058+
})
2059+
2060+
@api.route('/test/')
2061+
@api.response('200', 'Success')
2062+
class TestResource(BaseResource):
2063+
# @api.marshal_with also yields a response
2064+
@api.marshal_with(model, code=200, description='Success on method')
2065+
@api.response(404, 'Not Found on method')
2066+
def get(self):
2067+
{}
2068+
2069+
data = client.get_specs('')
2070+
paths = data['paths']
2071+
2072+
op = paths['/test/']['get']
2073+
print(op['responses'])
2074+
assert op['responses'] == {
2075+
'200': {
2076+
'description': 'Success on method',
2077+
'schema': {
2078+
'$ref': '#/definitions/SomeModel'
2079+
}
2080+
},
2081+
'404': {
2082+
'description': 'Not Found on method',
2083+
}
2084+
}
2085+
20452086
def test_clone(self, api, client):
20462087
parent = api.model('Person', {
20472088
'name': restplus.fields.String,

0 commit comments

Comments
 (0)