diff --git a/connexion/operation.py b/connexion/operation.py index d39683447..480fe5f98 100644 --- a/connexion/operation.py +++ b/connexion/operation.py @@ -11,6 +11,7 @@ language governing permissions and limitations under the License. """ +from copy import deepcopy import functools import logging import os @@ -121,8 +122,10 @@ def validate_defaults(self): param_type=param['type'])) def resolve_reference(self, schema): - schema = schema.copy() # avoid changing the original schema + schema = deepcopy(schema) # avoid changing the original schema reference = schema.get('$ref') # type: str + if not reference and 'items' in schema: + reference = schema['items'].get('$ref') if reference: if not reference.startswith('#/'): raise InvalidSpecification( @@ -136,11 +139,28 @@ def resolve_reference(self, schema): "{method} {path} '$ref' needs to point to definitions or parameters".format(**vars(self))) definition_name = path[-1] try: - schema.update(definitions[definition_name]) + # Get sub definition + definition = deepcopy(definitions[definition_name]) + for prop, prop_spec in definition.get('properties', {}).items(): + resolved = self.resolve_reference(prop_spec.get('schema', {})) + if resolved == {}: + resolved = self.resolve_reference(prop_spec) + + if not resolved == {}: + definition['properties'][prop] = resolved + + # Update schema + if '$ref' in schema: + schema.update(definition) + else: + schema['items'].update(definition) except KeyError: raise InvalidSpecification("{method} {path} Definition '{definition_name}' not found".format( definition_name=definition_name, method=self.method, path=self.path)) - del schema['$ref'] + if '$ref' in schema: + del schema['$ref'] + else: + del schema['items']['$ref'] return schema def get_mimetype(self): diff --git a/tests/test_operation.py b/tests/test_operation.py index 46e392bc8..7f01ac272 100644 --- a/tests/test_operation.py +++ b/tests/test_operation.py @@ -1,3 +1,4 @@ +from copy import deepcopy import pathlib import types @@ -22,7 +23,10 @@ 'description': 'YAML to provide to senza'}, 'new_traffic': {'type': 'integer', 'description': - 'Percentage of the traffic'}}}} + 'Percentage of the traffic'}}}, + 'composed': {'required': ['test'], + 'type': 'object', + 'properties': {'test': {'schema': {'$ref': '#/definitions/new_stack'}}}}} PARAMETER_DEFINITIONS = {'myparam': {'in': 'path', 'type': 'integer'}} OPERATION1 = {'description': 'Adds a new stack to be created by lizzy and returns the ' @@ -178,6 +182,50 @@ 'summary': 'Create new stack' } +OPERATION9 = {'description': 'Adds a new stack to be created by lizzy and returns the ' + 'information needed to keep track of deployment', + 'operationId': 'fakeapi.hello.post_greeting', + 'parameters': [{'in': 'body', + 'name': 'new_stack', + 'required': True, + 'schema': {'type': 'array', 'items': {'$ref': '#/definitions/new_stack'}}}], + 'responses': {201: {'description': 'Stack to be created. The ' + 'CloudFormation Stack creation can ' + "still fail if it's rejected by senza " + 'or AWS CF.', + 'schema': {'$ref': '#/definitions/stack'}}, + 400: {'description': 'Stack was not created because request ' + 'was invalid', + 'schema': {'$ref': '#/definitions/problem'}}, + 401: {'description': 'Stack was not created because the ' + 'access token was not provided or was ' + 'not valid for this operation', + 'schema': {'$ref': '#/definitions/problem'}}}, + 'security': [{'oauth': ['uid']}], + 'summary': 'Create new stack'} + +OPERATION10 = {'description': 'Adds a new stack to be created by lizzy and returns the ' + 'information needed to keep track of deployment', + 'operationId': 'fakeapi.hello.post_greeting', + 'parameters': [{'in': 'body', + 'name': 'test', + 'required': True, + 'schema': {'$ref': '#/definitions/composed'}}], + 'responses': {201: {'description': 'Stack to be created. The ' + 'CloudFormation Stack creation can ' + "still fail if it's rejected by senza " + 'or AWS CF.', + 'schema': {'$ref': '#/definitions/stack'}}, + 400: {'description': 'Stack was not created because request ' + 'was invalid', + 'schema': {'$ref': '#/definitions/problem'}}, + 401: {'description': 'Stack was not created because the ' + 'access token was not provided or was ' + 'not valid for this operation', + 'schema': {'$ref': '#/definitions/problem'}}}, + 'security': [{'oauth': ['uid']}], + 'summary': 'Create new stack'} + SECURITY_DEFINITIONS = {'oauth': {'type': 'oauth2', 'flow': 'password', 'x-tokenInfoUrl': 'https://ouath.example/token_info', @@ -210,6 +258,52 @@ def test_operation(): assert operation.body_schema == DEFINITIONS['new_stack'] +def test_operation_array(): + operation = Operation(method='GET', + path='endpoint', + operation=OPERATION9, + app_produces=['application/json'], + app_security=[], + security_definitions=SECURITY_DEFINITIONS, + definitions=DEFINITIONS, + parameter_definitions=PARAMETER_DEFINITIONS, + resolver=Resolver()) + assert isinstance(operation.function, types.FunctionType) + # security decorator should be a partial with verify_oauth as the function and token url and scopes as arguments. + # See https://docs.python.org/2/library/functools.html#partial-objects + assert operation._Operation__security_decorator.func is verify_oauth + assert operation._Operation__security_decorator.args == ('https://ouath.example/token_info', set(['uid'])) + + assert operation.method == 'GET' + assert operation.produces == ['application/json'] + assert operation.security == [{'oauth': ['uid']}] + assert operation.body_schema == {'type': 'array', 'items': DEFINITIONS['new_stack']} + + +def test_operation_composed_definition(): + operation = Operation(method='GET', + path='endpoint', + operation=OPERATION10, + app_produces=['application/json'], + app_security=[], + security_definitions=SECURITY_DEFINITIONS, + definitions=DEFINITIONS, + parameter_definitions=PARAMETER_DEFINITIONS, + resolver=Resolver()) + assert isinstance(operation.function, types.FunctionType) + # security decorator should be a partial with verify_oauth as the function and token url and scopes as arguments. + # See https://docs.python.org/2/library/functools.html#partial-objects + assert operation._Operation__security_decorator.func is verify_oauth + assert operation._Operation__security_decorator.args == ('https://ouath.example/token_info', set(['uid'])) + + assert operation.method == 'GET' + assert operation.produces == ['application/json'] + assert operation.security == [{'oauth': ['uid']}] + definition = deepcopy(DEFINITIONS['composed']) + definition['properties']['test'] = DEFINITIONS['new_stack'] + assert operation.body_schema == definition + + def test_non_existent_reference(): operation = Operation(method='GET', path='endpoint',