mirror of
https://github.com/LukeHagar/connexion.git
synced 2025-12-11 04:19:36 +00:00
Merge pull request #126 from jbryan/better_reference_handling
Added support for circular references and additionalProperties
This commit is contained in:
@@ -45,10 +45,11 @@ class ResponseValidator(BaseDecorator):
|
|||||||
if not response_definitions:
|
if not response_definitions:
|
||||||
return True
|
return True
|
||||||
response_definition = response_definitions.get(status_code, {})
|
response_definition = response_definitions.get(status_code, {})
|
||||||
|
response_definition = self.operation.resolve_reference(response_definition)
|
||||||
# TODO handle default response definitions
|
# TODO handle default response definitions
|
||||||
|
|
||||||
if response_definition and response_definition.get("schema"):
|
if response_definition and response_definition.get("schema"):
|
||||||
schema = self.operation.resolve_reference(response_definition.get("schema"))
|
schema = response_definition.get("schema")
|
||||||
v = RequestBodyValidator(schema)
|
v = RequestBodyValidator(schema)
|
||||||
error = v.validate_schema(data, schema)
|
error = v.validate_schema(data, schema)
|
||||||
if error:
|
if error:
|
||||||
|
|||||||
@@ -123,12 +123,57 @@ class Operation:
|
|||||||
|
|
||||||
def resolve_reference(self, schema):
|
def resolve_reference(self, schema):
|
||||||
schema = deepcopy(schema) # avoid changing the original schema
|
schema = deepcopy(schema) # avoid changing the original schema
|
||||||
# find the object we need to resolve/update
|
self.check_references(schema)
|
||||||
|
|
||||||
|
# find the object we need to resolve/update if this is not a proper SchemaObject
|
||||||
|
# e.g a response or parameter object
|
||||||
for obj in schema, schema.get('items'):
|
for obj in schema, schema.get('items'):
|
||||||
reference = obj and obj.get('$ref') # type: str
|
reference = obj and obj.get('$ref') # type: str
|
||||||
if reference:
|
if reference:
|
||||||
break
|
break
|
||||||
if reference:
|
if reference:
|
||||||
|
definition = deepcopy(self._retrieve_reference(reference))
|
||||||
|
# Update schema
|
||||||
|
obj.update(definition)
|
||||||
|
del obj['$ref']
|
||||||
|
|
||||||
|
# if there is a schema object on this param or response, then we just
|
||||||
|
# need to include the defs and it can be validated by jsonschema
|
||||||
|
if 'schema' in schema:
|
||||||
|
schema['schema']['definitions'] = self.definitions
|
||||||
|
return schema
|
||||||
|
|
||||||
|
return schema
|
||||||
|
|
||||||
|
def check_references(self, schema):
|
||||||
|
"""
|
||||||
|
Searches the keys and values of a schema object for json references.
|
||||||
|
If it finds one, it attempts to locate it and will thrown an exception
|
||||||
|
if the reference can't be found in the definitions dictionary.
|
||||||
|
|
||||||
|
:param schema: The schema object to check
|
||||||
|
:type schema: dict
|
||||||
|
:raises InvalidSpecification: raised when a reference isn't found
|
||||||
|
"""
|
||||||
|
|
||||||
|
stack = [schema]
|
||||||
|
visited = set()
|
||||||
|
while stack:
|
||||||
|
schema = stack.pop()
|
||||||
|
for k, v in schema.items():
|
||||||
|
if k == "$ref":
|
||||||
|
if v in visited:
|
||||||
|
continue
|
||||||
|
visited.add(v)
|
||||||
|
stack.append(self._retrieve_reference(v))
|
||||||
|
elif isinstance(v, (list, tuple)):
|
||||||
|
for item in v:
|
||||||
|
if hasattr(item, "items"):
|
||||||
|
stack.append(item)
|
||||||
|
elif hasattr(v, "items"):
|
||||||
|
stack.append(v)
|
||||||
|
|
||||||
|
def _retrieve_reference(self, reference):
|
||||||
if not reference.startswith('#/'):
|
if not reference.startswith('#/'):
|
||||||
raise InvalidSpecification(
|
raise InvalidSpecification(
|
||||||
"{method} {path} '$ref' needs to start with '#/'".format(**vars(self)))
|
"{method} {path} '$ref' needs to start with '#/'".format(**vars(self)))
|
||||||
@@ -147,19 +192,7 @@ class Operation:
|
|||||||
raise InvalidSpecification("{method} {path} Definition '{definition_name}' not found".format(
|
raise InvalidSpecification("{method} {path} Definition '{definition_name}' not found".format(
|
||||||
definition_name=definition_name, method=self.method, path=self.path))
|
definition_name=definition_name, method=self.method, path=self.path))
|
||||||
|
|
||||||
# resolve object properties too
|
return definition
|
||||||
for prop, prop_spec in definition.get('properties', {}).items():
|
|
||||||
resolved = self.resolve_reference(prop_spec.get('schema', {}))
|
|
||||||
if not resolved:
|
|
||||||
resolved = self.resolve_reference(prop_spec)
|
|
||||||
|
|
||||||
if resolved:
|
|
||||||
definition['properties'][prop] = resolved
|
|
||||||
|
|
||||||
# Update schema
|
|
||||||
obj.update(definition)
|
|
||||||
del obj['$ref']
|
|
||||||
return schema
|
|
||||||
|
|
||||||
def get_mimetype(self):
|
def get_mimetype(self):
|
||||||
if produces_json(self.produces): # endpoint will return json
|
if produces_json(self.produces): # endpoint will return json
|
||||||
@@ -202,9 +235,6 @@ class Operation:
|
|||||||
|
|
||||||
body_parameters = body_parameters[0] if body_parameters else {}
|
body_parameters = body_parameters[0] if body_parameters else {}
|
||||||
schema = body_parameters.get('schema') # type: dict
|
schema = body_parameters.get('schema') # type: dict
|
||||||
|
|
||||||
if schema:
|
|
||||||
schema = self.resolve_reference(schema)
|
|
||||||
return schema
|
return schema
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -215,15 +245,7 @@ class Operation:
|
|||||||
:rtype: types.FunctionType
|
:rtype: types.FunctionType
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parameters = []
|
function = parameter_to_arg(self.parameters, self.__undecorated_function)
|
||||||
for param in self.parameters: # resolve references
|
|
||||||
param = param.copy()
|
|
||||||
schema = param.get('schema')
|
|
||||||
if schema:
|
|
||||||
schema = self.resolve_reference(schema)
|
|
||||||
param['schema'] = schema
|
|
||||||
parameters.append(param)
|
|
||||||
function = parameter_to_arg(parameters, self.__undecorated_function)
|
|
||||||
|
|
||||||
if self.validate_responses:
|
if self.validate_responses:
|
||||||
logger.debug('... Response validation enabled.')
|
logger.debug('... Response validation enabled.')
|
||||||
|
|||||||
@@ -352,6 +352,44 @@ paths:
|
|||||||
description: goodbye response
|
description: goodbye response
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
/test_schema_map:
|
||||||
|
post:
|
||||||
|
summary: Returns empty response
|
||||||
|
description: Returns empty response
|
||||||
|
operationId: fakeapi.hello.schema_map
|
||||||
|
parameters:
|
||||||
|
- name: new_stack
|
||||||
|
required: true
|
||||||
|
in: body
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
$ref: '#/definitions/new_stack'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: goodbye response
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
/test_schema_recursive:
|
||||||
|
post:
|
||||||
|
summary: Returns empty response
|
||||||
|
description: Returns empty response
|
||||||
|
operationId: fakeapi.hello.schema_recursive
|
||||||
|
parameters:
|
||||||
|
- name: new_stack
|
||||||
|
required: true
|
||||||
|
in: body
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/simple_tree'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: goodbye response
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
/test_schema_format:
|
/test_schema_format:
|
||||||
post:
|
post:
|
||||||
summary: Returns empty response
|
summary: Returns empty response
|
||||||
@@ -681,3 +719,12 @@ definitions:
|
|||||||
description: Docker image version to deploy
|
description: Docker image version to deploy
|
||||||
required:
|
required:
|
||||||
- image_version
|
- image_version
|
||||||
|
simple_tree:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
children:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/definitions/simple_tree"
|
||||||
|
description: Docker image version to deploy
|
||||||
|
additionalProperties: false
|
||||||
|
|||||||
@@ -160,6 +160,11 @@ def schema_query(image_version=None):
|
|||||||
def schema_list():
|
def schema_list():
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
def schema_map():
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def schema_recursive():
|
||||||
|
return ''
|
||||||
|
|
||||||
def schema_format():
|
def schema_format():
|
||||||
return ''
|
return ''
|
||||||
|
|||||||
@@ -397,6 +397,78 @@ def test_schema_list(app):
|
|||||||
assert wrong_items_response['title'] == 'Bad Request'
|
assert wrong_items_response['title'] == 'Bad Request'
|
||||||
assert wrong_items_response['detail'].startswith("42 is not of type 'string'")
|
assert wrong_items_response['detail'].startswith("42 is not of type 'string'")
|
||||||
|
|
||||||
|
def test_schema_map(app):
|
||||||
|
app_client = app.app.test_client()
|
||||||
|
headers = {'Content-type': 'application/json'}
|
||||||
|
|
||||||
|
valid_object = {
|
||||||
|
"foo": {
|
||||||
|
"image_version": "string"
|
||||||
|
},
|
||||||
|
"bar": {
|
||||||
|
"image_version": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invalid_object = {
|
||||||
|
"foo": 42
|
||||||
|
}
|
||||||
|
|
||||||
|
wrong_type = app_client.post('/v1.0/test_schema_map', headers=headers, data=json.dumps(42)) # type: flask.Response
|
||||||
|
assert wrong_type.status_code == 400
|
||||||
|
assert wrong_type.content_type == 'application/problem+json'
|
||||||
|
wrong_type_response = json.loads(wrong_type.data.decode()) # type: dict
|
||||||
|
assert wrong_type_response['title'] == 'Bad Request'
|
||||||
|
assert wrong_type_response['detail'].startswith("42 is not of type 'object'")
|
||||||
|
|
||||||
|
wrong_items = app_client.post('/v1.0/test_schema_map', headers=headers,
|
||||||
|
data=json.dumps(invalid_object)) # type: flask.Response
|
||||||
|
assert wrong_items.status_code == 400
|
||||||
|
assert wrong_items.content_type == 'application/problem+json'
|
||||||
|
wrong_items_response = json.loads(wrong_items.data.decode()) # type: dict
|
||||||
|
assert wrong_items_response['title'] == 'Bad Request'
|
||||||
|
assert wrong_items_response['detail'].startswith("42 is not of type 'object'")
|
||||||
|
|
||||||
|
right_type = app_client.post('/v1.0/test_schema_map', headers=headers,
|
||||||
|
data=json.dumps(valid_object)) # type: flask.Response
|
||||||
|
assert right_type.status_code == 200
|
||||||
|
|
||||||
|
def test_schema_recursive(app):
|
||||||
|
app_client = app.app.test_client()
|
||||||
|
headers = {'Content-type': 'application/json'}
|
||||||
|
|
||||||
|
valid_object = {
|
||||||
|
"children": [
|
||||||
|
{"children": []},
|
||||||
|
{"children": [
|
||||||
|
{"children": []},
|
||||||
|
]},
|
||||||
|
{"children": []},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
invalid_object = {
|
||||||
|
"children": [42]
|
||||||
|
}
|
||||||
|
|
||||||
|
wrong_type = app_client.post('/v1.0/test_schema_recursive', headers=headers, data=json.dumps(42)) # type: flask.Response
|
||||||
|
assert wrong_type.status_code == 400
|
||||||
|
assert wrong_type.content_type == 'application/problem+json'
|
||||||
|
wrong_type_response = json.loads(wrong_type.data.decode()) # type: dict
|
||||||
|
assert wrong_type_response['title'] == 'Bad Request'
|
||||||
|
assert wrong_type_response['detail'].startswith("42 is not of type 'object'")
|
||||||
|
|
||||||
|
wrong_items = app_client.post('/v1.0/test_schema_recursive', headers=headers,
|
||||||
|
data=json.dumps(invalid_object)) # type: flask.Response
|
||||||
|
assert wrong_items.status_code == 400
|
||||||
|
assert wrong_items.content_type == 'application/problem+json'
|
||||||
|
wrong_items_response = json.loads(wrong_items.data.decode()) # type: dict
|
||||||
|
assert wrong_items_response['title'] == 'Bad Request'
|
||||||
|
assert wrong_items_response['detail'].startswith("42 is not of type 'object'")
|
||||||
|
|
||||||
|
right_type = app_client.post('/v1.0/test_schema_recursive', headers=headers,
|
||||||
|
data=json.dumps(valid_object)) # type: flask.Response
|
||||||
|
assert right_type.status_code == 200
|
||||||
|
|
||||||
def test_schema_format(app):
|
def test_schema_format(app):
|
||||||
app_client = app.app.test_client()
|
app_client = app.app.test_client()
|
||||||
|
|||||||
@@ -255,7 +255,12 @@ def test_operation():
|
|||||||
assert operation.method == 'GET'
|
assert operation.method == 'GET'
|
||||||
assert operation.produces == ['application/json']
|
assert operation.produces == ['application/json']
|
||||||
assert operation.security == [{'oauth': ['uid']}]
|
assert operation.security == [{'oauth': ['uid']}]
|
||||||
assert operation.body_schema == DEFINITIONS['new_stack']
|
|
||||||
|
expected_body_schema = {
|
||||||
|
'$ref': '#/definitions/new_stack',
|
||||||
|
'definitions': DEFINITIONS
|
||||||
|
}
|
||||||
|
assert operation.body_schema == expected_body_schema
|
||||||
|
|
||||||
|
|
||||||
def test_operation_array():
|
def test_operation_array():
|
||||||
@@ -277,7 +282,12 @@ def test_operation_array():
|
|||||||
assert operation.method == 'GET'
|
assert operation.method == 'GET'
|
||||||
assert operation.produces == ['application/json']
|
assert operation.produces == ['application/json']
|
||||||
assert operation.security == [{'oauth': ['uid']}]
|
assert operation.security == [{'oauth': ['uid']}]
|
||||||
assert operation.body_schema == {'type': 'array', 'items': DEFINITIONS['new_stack']}
|
expected_body_schema = {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {'$ref': '#/definitions/new_stack'},
|
||||||
|
'definitions': DEFINITIONS
|
||||||
|
}
|
||||||
|
assert operation.body_schema == expected_body_schema
|
||||||
|
|
||||||
|
|
||||||
def test_operation_composed_definition():
|
def test_operation_composed_definition():
|
||||||
@@ -299,12 +309,15 @@ def test_operation_composed_definition():
|
|||||||
assert operation.method == 'GET'
|
assert operation.method == 'GET'
|
||||||
assert operation.produces == ['application/json']
|
assert operation.produces == ['application/json']
|
||||||
assert operation.security == [{'oauth': ['uid']}]
|
assert operation.security == [{'oauth': ['uid']}]
|
||||||
definition = deepcopy(DEFINITIONS['composed'])
|
expected_body_schema = {
|
||||||
definition['properties']['test'] = DEFINITIONS['new_stack']
|
'$ref': '#/definitions/composed',
|
||||||
assert operation.body_schema == definition
|
'definitions': DEFINITIONS
|
||||||
|
}
|
||||||
|
assert operation.body_schema == expected_body_schema
|
||||||
|
|
||||||
|
|
||||||
def test_non_existent_reference():
|
def test_non_existent_reference():
|
||||||
|
with pytest.raises(InvalidSpecification) as exc_info: # type: py.code.ExceptionInfo
|
||||||
operation = Operation(method='GET',
|
operation = Operation(method='GET',
|
||||||
path='endpoint',
|
path='endpoint',
|
||||||
operation=OPERATION1,
|
operation=OPERATION1,
|
||||||
@@ -314,7 +327,6 @@ def test_non_existent_reference():
|
|||||||
definitions={},
|
definitions={},
|
||||||
parameter_definitions={},
|
parameter_definitions={},
|
||||||
resolver=Resolver())
|
resolver=Resolver())
|
||||||
with pytest.raises(InvalidSpecification) as exc_info: # type: py.code.ExceptionInfo
|
|
||||||
schema = operation.body_schema
|
schema = operation.body_schema
|
||||||
|
|
||||||
exception = exc_info.value
|
exception = exc_info.value
|
||||||
@@ -323,6 +335,7 @@ def test_non_existent_reference():
|
|||||||
|
|
||||||
|
|
||||||
def test_multi_body():
|
def test_multi_body():
|
||||||
|
with pytest.raises(InvalidSpecification) as exc_info: # type: py.code.ExceptionInfo
|
||||||
operation = Operation(method='GET',
|
operation = Operation(method='GET',
|
||||||
path='endpoint',
|
path='endpoint',
|
||||||
operation=OPERATION2,
|
operation=OPERATION2,
|
||||||
@@ -332,7 +345,6 @@ def test_multi_body():
|
|||||||
definitions=DEFINITIONS,
|
definitions=DEFINITIONS,
|
||||||
parameter_definitions=PARAMETER_DEFINITIONS,
|
parameter_definitions=PARAMETER_DEFINITIONS,
|
||||||
resolver=Resolver())
|
resolver=Resolver())
|
||||||
with pytest.raises(InvalidSpecification) as exc_info: # type: py.code.ExceptionInfo
|
|
||||||
schema = operation.body_schema
|
schema = operation.body_schema
|
||||||
|
|
||||||
exception = exc_info.value
|
exception = exc_info.value
|
||||||
@@ -341,6 +353,7 @@ def test_multi_body():
|
|||||||
|
|
||||||
|
|
||||||
def test_invalid_reference():
|
def test_invalid_reference():
|
||||||
|
with pytest.raises(InvalidSpecification) as exc_info: # type: py.code.ExceptionInfo
|
||||||
operation = Operation(method='GET',
|
operation = Operation(method='GET',
|
||||||
path='endpoint',
|
path='endpoint',
|
||||||
operation=OPERATION3,
|
operation=OPERATION3,
|
||||||
@@ -350,7 +363,6 @@ def test_invalid_reference():
|
|||||||
definitions=DEFINITIONS,
|
definitions=DEFINITIONS,
|
||||||
parameter_definitions=PARAMETER_DEFINITIONS,
|
parameter_definitions=PARAMETER_DEFINITIONS,
|
||||||
resolver=Resolver())
|
resolver=Resolver())
|
||||||
with pytest.raises(InvalidSpecification) as exc_info: # type: py.code.ExceptionInfo
|
|
||||||
schema = operation.body_schema
|
schema = operation.body_schema
|
||||||
|
|
||||||
exception = exc_info.value
|
exception = exc_info.value
|
||||||
@@ -374,7 +386,12 @@ def test_no_token_info():
|
|||||||
assert operation.method == 'GET'
|
assert operation.method == 'GET'
|
||||||
assert operation.produces == ['application/json']
|
assert operation.produces == ['application/json']
|
||||||
assert operation.security == [{'oauth': ['uid']}]
|
assert operation.security == [{'oauth': ['uid']}]
|
||||||
assert operation.body_schema == DEFINITIONS['new_stack']
|
|
||||||
|
expected_body_schema = {
|
||||||
|
'$ref': '#/definitions/new_stack',
|
||||||
|
'definitions': DEFINITIONS
|
||||||
|
}
|
||||||
|
assert operation.body_schema == expected_body_schema
|
||||||
|
|
||||||
|
|
||||||
def test_parameter_reference():
|
def test_parameter_reference():
|
||||||
@@ -403,7 +420,7 @@ def test_resolve_invalid_reference():
|
|||||||
def test_bad_default():
|
def test_bad_default():
|
||||||
with pytest.raises(InvalidSpecification) as exc_info:
|
with pytest.raises(InvalidSpecification) as exc_info:
|
||||||
Operation(method='GET', path='endpoint', operation=OPERATION6, app_produces=['application/json'],
|
Operation(method='GET', path='endpoint', operation=OPERATION6, app_produces=['application/json'],
|
||||||
app_security=[], security_definitions={}, definitions={}, parameter_definitions=PARAMETER_DEFINITIONS,
|
app_security=[], security_definitions={}, definitions=DEFINITIONS, parameter_definitions=PARAMETER_DEFINITIONS,
|
||||||
resolver=Resolver())
|
resolver=Resolver())
|
||||||
exception = exc_info.value
|
exception = exc_info.value
|
||||||
assert str(exception) == "<InvalidSpecification: The parameter 'stack_version' has a default value which " \
|
assert str(exception) == "<InvalidSpecification: The parameter 'stack_version' has a default value which " \
|
||||||
|
|||||||
Reference in New Issue
Block a user