Skip to content

Commit

Permalink
fix nested additionalProperties (spec-first#1138)
Browse files Browse the repository at this point in the history
  • Loading branch information
dtkav committed Jan 29, 2020
1 parent 4b93890 commit 3fecd3e
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 17 deletions.
28 changes: 12 additions & 16 deletions connexion/operations/openapi.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logging
from copy import deepcopy
from copy import copy, deepcopy

from connexion.operations.abstract import AbstractOperation

Expand Down Expand Up @@ -303,23 +303,30 @@ def _get_typed_body_values(self, body_arg, body_props, additional_props):

return res

def _build_default_obj(self, _properties, res={}):
def _build_default_obj_recursive(self, _properties, res):
""" takes disparate and nested default keys, and builds up a default object
"""
for key, prop in _properties.items():
if 'default' in prop and key not in res:
res[key] = prop['default']
res[key] = copy(prop['default'])
elif prop.get('type') == 'object' and 'properties' in prop:
res.setdefault(key, {})
res[key] = self._build_default_obj(prop['properties'], res[key])
res[key] = self._build_default_obj_recursive(prop['properties'], res[key])
return res

def _get_default_obj(self, schema):
try:
return deepcopy(schema["default"])
except KeyError:
_properties = schema.get("properties", {})
return self._build_default_obj_recursive(_properties, {})

def _get_query_defaults(self, query_defns):
defaults = {}
for k, v in query_defns.items():
try:
if v["schema"]["type"] == "object":
defaults[k] = self._build_default_obj(v["schema"]["properties"])
defaults[k] = self._get_default_obj(v["schema"])
else:
defaults[k] = v["schema"]["default"]
except KeyError:
Expand All @@ -345,16 +352,5 @@ def _get_val_from_param(self, value, query_defn):

if query_schema["type"] == "array":
return [make_type(part, query_schema["items"]["type"]) for part in value]
elif query_schema["type"] == "object" and 'properties' in query_schema:
return_dict = {}
for prop_key in query_schema['properties'].keys():
prop_value = value.get(prop_key, None)
if prop_value is not None: # False is a valid value for boolean values
try:
return_dict[prop_key] = make_type(value[prop_key],
query_schema['properties'][prop_key]['type'])
except (KeyError, TypeError):
return value
return return_dict
else:
return make_type(value, query_schema["type"])
20 changes: 19 additions & 1 deletion tests/api/test_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ def test_empty(simple_app):
def test_exploded_deep_object_param_endpoint_openapi_simple(simple_openapi_app):
app_client = simple_openapi_app.app.test_client()

response = app_client.get('/v1.0/exploded-deep-object-param?id[foo]=bar&id[foofoo]=barbar') # type: flask.Response
response = app_client.get('/v1.0/exploded-deep-object-param?id[foo]=bar') # type: flask.Response
assert response.status_code == 200
response_data = json.loads(response.data.decode('utf-8', 'replace'))
assert response_data == {'foo': 'bar', 'foo4': 'blubb'}
Expand All @@ -166,6 +166,13 @@ def test_exploded_deep_object_param_endpoint_openapi_additional_properties(simpl
assert response_data == {'foo': 'bar', 'fooint': '2'}


def test_exploded_deep_object_param_endpoint_openapi_additional_properties_false(simple_openapi_app):
app_client = simple_openapi_app.app.test_client()

response = app_client.get('/v1.0/exploded-deep-object-param?id[foo]=bar&id[foofoo]=barbar') # type: flask.Response
assert response.status_code == 400


def test_exploded_deep_object_param_endpoint_openapi_with_dots(simple_openapi_app):
app_client = simple_openapi_app.app.test_client()

Expand Down Expand Up @@ -220,6 +227,17 @@ def test_empty_object_body(simple_app):
assert response['stack'] == {}


def test_nested_additional_properties(simple_openapi_app):
app_client = simple_openapi_app.app.test_client()
resp = app_client.post(
'/v1.0/test-nested-additional-properties',
data=json.dumps({"nested": {"object": True}}),
headers={'Content-Type': 'application/json'})
assert resp.status_code == 200
response = json.loads(resp.data.decode('utf-8', 'replace'))
assert response == {"nested": {"object": True}}


def test_custom_encoder(simple_app):

class CustomEncoder(FlaskJSONEncoder):
Expand Down
2 changes: 2 additions & 0 deletions tests/fakeapi/hello.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,8 @@ def test_default_param(name):
def test_default_object_body(stack):
return {"stack": stack}

def test_nested_additional_properties(body):
return body

def test_default_integer_body(stack_version):
return stack_version
Expand Down
19 changes: 19 additions & 0 deletions tests/fixtures/simple/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ paths:
explode: true
schema:
type: object
additionalProperties: false
properties:
foo:
type: string
Expand Down Expand Up @@ -277,6 +278,24 @@ paths:
$ref: '#/components/schemas/new_stack'
default:
image_version: default_image
/test-nested-additional-properties:
post:
summary: Test if nested additionalProperties are cast
operationId: fakeapi.hello.test_nested_additional_properties
responses:
'200':
description: OK
requestBody:
content:
application/json:
schema:
type: object
properties:
nested:
type: object
properties: {}
additionalProperties:
type: boolean
/test-default-integer-body:
post:
summary: Test if default integer body param is passed to handler.
Expand Down

0 comments on commit 3fecd3e

Please sign in to comment.