mirror of
https://github.com/correl/openapi-core.git
synced 2024-11-22 03:00:10 +00:00
Raw value type strict validation
This commit is contained in:
parent
395f68b234
commit
cd0ca5d26f
6 changed files with 317 additions and 42 deletions
|
@ -109,7 +109,11 @@ class Parameter(object):
|
|||
raise InvalidParameterValue(self.name, exc)
|
||||
|
||||
try:
|
||||
unmarshalled = self.schema.unmarshal(deserialized, custom_formatters=custom_formatters)
|
||||
unmarshalled = self.schema.unmarshal(
|
||||
deserialized,
|
||||
custom_formatters=custom_formatters,
|
||||
strict=False,
|
||||
)
|
||||
except OpenAPISchemaError as exc:
|
||||
raise InvalidParameterValue(self.name, exc)
|
||||
|
||||
|
|
|
@ -38,9 +38,6 @@ class Schema(object):
|
|||
"""Represents an OpenAPI Schema."""
|
||||
|
||||
DEFAULT_CAST_CALLABLE_GETTER = {
|
||||
SchemaType.INTEGER: int,
|
||||
SchemaType.NUMBER: float,
|
||||
SchemaType.BOOLEAN: forcebool,
|
||||
}
|
||||
|
||||
STRING_FORMAT_CALLABLE_GETTER = {
|
||||
|
@ -148,12 +145,15 @@ class Schema(object):
|
|||
|
||||
return set(required)
|
||||
|
||||
def get_cast_mapping(self, custom_formatters=None):
|
||||
def get_cast_mapping(self, custom_formatters=None, strict=True):
|
||||
pass_defaults = lambda f: functools.partial(
|
||||
f, custom_formatters=custom_formatters)
|
||||
f, custom_formatters=custom_formatters, strict=strict)
|
||||
mapping = self.DEFAULT_CAST_CALLABLE_GETTER.copy()
|
||||
mapping.update({
|
||||
SchemaType.STRING: pass_defaults(self._unmarshal_string),
|
||||
SchemaType.BOOLEAN: pass_defaults(self._unmarshal_boolean),
|
||||
SchemaType.INTEGER: pass_defaults(self._unmarshal_integer),
|
||||
SchemaType.NUMBER: pass_defaults(self._unmarshal_number),
|
||||
SchemaType.ANY: pass_defaults(self._unmarshal_any),
|
||||
SchemaType.ARRAY: pass_defaults(self._unmarshal_collection),
|
||||
SchemaType.OBJECT: pass_defaults(self._unmarshal_object),
|
||||
|
@ -161,14 +161,15 @@ class Schema(object):
|
|||
|
||||
return defaultdict(lambda: lambda x: x, mapping)
|
||||
|
||||
def cast(self, value, custom_formatters=None):
|
||||
def cast(self, value, custom_formatters=None, strict=True):
|
||||
"""Cast value to schema type"""
|
||||
if value is None:
|
||||
if not self.nullable:
|
||||
raise InvalidSchemaValue("Null value for non-nullable schema", value, self.type)
|
||||
return self.default
|
||||
|
||||
cast_mapping = self.get_cast_mapping(custom_formatters=custom_formatters)
|
||||
cast_mapping = self.get_cast_mapping(
|
||||
custom_formatters=custom_formatters, strict=strict)
|
||||
|
||||
if self.type is not SchemaType.STRING and value == '':
|
||||
return None
|
||||
|
@ -180,12 +181,12 @@ class Schema(object):
|
|||
raise InvalidSchemaValue(
|
||||
"Failed to cast value {value} to type {type}", value, self.type)
|
||||
|
||||
def unmarshal(self, value, custom_formatters=None):
|
||||
def unmarshal(self, value, custom_formatters=None, strict=True):
|
||||
"""Unmarshal parameter from the value."""
|
||||
if self.deprecated:
|
||||
warnings.warn("The schema is deprecated", DeprecationWarning)
|
||||
|
||||
casted = self.cast(value, custom_formatters=custom_formatters)
|
||||
casted = self.cast(value, custom_formatters=custom_formatters, strict=strict)
|
||||
|
||||
if casted is None and not self.required:
|
||||
return None
|
||||
|
@ -196,7 +197,10 @@ class Schema(object):
|
|||
|
||||
return casted
|
||||
|
||||
def _unmarshal_string(self, value, custom_formatters=None):
|
||||
def _unmarshal_string(self, value, custom_formatters=None, strict=True):
|
||||
if strict and not isinstance(value, (text_type, binary_type)):
|
||||
raise InvalidSchemaValue("Value {value} is not of type {type}", value, self.type)
|
||||
|
||||
try:
|
||||
schema_format = SchemaFormat(self.format)
|
||||
except ValueError:
|
||||
|
@ -216,7 +220,25 @@ class Schema(object):
|
|||
raise InvalidCustomFormatSchemaValue(
|
||||
"Failed to format value {value} to format {type}: {exception}", value, self.format, exc)
|
||||
|
||||
def _unmarshal_any(self, value, custom_formatters=None):
|
||||
def _unmarshal_integer(self, value, custom_formatters=None, strict=True):
|
||||
if strict and not isinstance(value, (integer_types, )):
|
||||
raise InvalidSchemaValue("Value {value} is not of type {type}", value, self.type)
|
||||
|
||||
return int(value)
|
||||
|
||||
def _unmarshal_number(self, value, custom_formatters=None, strict=True):
|
||||
if strict and not isinstance(value, (float, )):
|
||||
raise InvalidSchemaValue("Value {value} is not of type {type}", value, self.type)
|
||||
|
||||
return float(value)
|
||||
|
||||
def _unmarshal_boolean(self, value, custom_formatters=None, strict=True):
|
||||
if strict and not isinstance(value, (bool, )):
|
||||
raise InvalidSchemaValue("Value {value} is not of type {type}", value, self.type)
|
||||
|
||||
return forcebool(value)
|
||||
|
||||
def _unmarshal_any(self, value, custom_formatters=None, strict=True):
|
||||
types_resolve_order = [
|
||||
SchemaType.OBJECT, SchemaType.ARRAY, SchemaType.BOOLEAN,
|
||||
SchemaType.INTEGER, SchemaType.NUMBER, SchemaType.STRING,
|
||||
|
@ -232,16 +254,21 @@ class Schema(object):
|
|||
|
||||
raise NoValidSchema(value)
|
||||
|
||||
def _unmarshal_collection(self, value, custom_formatters=None):
|
||||
def _unmarshal_collection(self, value, custom_formatters=None, strict=True):
|
||||
if not isinstance(value, (list, tuple)):
|
||||
raise InvalidSchemaValue("Value {value} is not of type {type}", value, self.type)
|
||||
|
||||
if self.items is None:
|
||||
raise UndefinedItemsSchema(self.type)
|
||||
|
||||
f = functools.partial(self.items.unmarshal,
|
||||
custom_formatters=custom_formatters)
|
||||
f = functools.partial(
|
||||
self.items.unmarshal,
|
||||
custom_formatters=custom_formatters, strict=strict,
|
||||
)
|
||||
return list(map(f, value))
|
||||
|
||||
def _unmarshal_object(self, value, model_factory=None,
|
||||
custom_formatters=None):
|
||||
custom_formatters=None, strict=True):
|
||||
if not isinstance(value, (dict, )):
|
||||
raise InvalidSchemaValue("Value {value} is not of type {type}", value, self.type)
|
||||
|
||||
|
@ -270,7 +297,7 @@ class Schema(object):
|
|||
return model_factory.create(properties, name=self.model)
|
||||
|
||||
def _unmarshal_properties(self, value, one_of_schema=None,
|
||||
custom_formatters=None):
|
||||
custom_formatters=None, strict=True):
|
||||
all_props = self.get_all_properties()
|
||||
all_props_names = self.get_all_properties_names()
|
||||
all_req_props_names = self.get_all_required_properties_names()
|
||||
|
|
|
@ -59,16 +59,7 @@ paths:
|
|||
explode: false
|
||||
responses:
|
||||
'200':
|
||||
description: An paged array of pets
|
||||
headers:
|
||||
x-next:
|
||||
description: A link to the next page of responses
|
||||
schema:
|
||||
type: string
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PetsData"
|
||||
$ref: "#/components/responses/PetsResponse"
|
||||
post:
|
||||
summary: Create a pet
|
||||
operationId: createPets
|
||||
|
@ -327,9 +318,20 @@ components:
|
|||
additionalProperties:
|
||||
type: string
|
||||
responses:
|
||||
ErrorResponse:
|
||||
description: unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ExtendedError"
|
||||
ErrorResponse:
|
||||
description: unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ExtendedError"
|
||||
PetsResponse:
|
||||
description: An paged array of pets
|
||||
headers:
|
||||
x-next:
|
||||
description: A link to the next page of responses
|
||||
schema:
|
||||
type: string
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PetsData"
|
||||
|
|
|
@ -17,8 +17,9 @@ from openapi_core.schema.parameters.models import Parameter
|
|||
from openapi_core.schema.paths.models import Path
|
||||
from openapi_core.schema.request_bodies.models import RequestBody
|
||||
from openapi_core.schema.responses.models import Response
|
||||
from openapi_core.schema.schemas.enums import SchemaType
|
||||
from openapi_core.schema.schemas.exceptions import (
|
||||
NoValidSchema,
|
||||
NoValidSchema, InvalidSchemaProperty, InvalidSchemaValue,
|
||||
)
|
||||
from openapi_core.schema.schemas.models import Schema
|
||||
from openapi_core.schema.servers.exceptions import InvalidServer
|
||||
|
@ -234,6 +235,105 @@ class TestPetstore(object):
|
|||
assert isinstance(response_result.data, BaseModel)
|
||||
assert response_result.data.data == []
|
||||
|
||||
def test_get_pets_response(self, spec, response_validator):
|
||||
host_url = 'http://petstore.swagger.io/v1'
|
||||
path_pattern = '/v1/pets'
|
||||
query_params = {
|
||||
'limit': '20',
|
||||
}
|
||||
|
||||
request = MockRequest(
|
||||
host_url, 'GET', '/pets',
|
||||
path_pattern=path_pattern, args=query_params,
|
||||
)
|
||||
|
||||
parameters = request.get_parameters(spec)
|
||||
body = request.get_body(spec)
|
||||
|
||||
assert parameters == {
|
||||
'query': {
|
||||
'limit': 20,
|
||||
'page': 1,
|
||||
'search': '',
|
||||
}
|
||||
}
|
||||
assert body is None
|
||||
|
||||
data_json = {
|
||||
'data': [
|
||||
{
|
||||
'id': 1,
|
||||
'name': 'Cat',
|
||||
}
|
||||
],
|
||||
}
|
||||
data = json.dumps(data_json)
|
||||
response = MockResponse(data)
|
||||
|
||||
response_result = response_validator.validate(request, response)
|
||||
|
||||
assert response_result.errors == []
|
||||
assert isinstance(response_result.data, BaseModel)
|
||||
assert len(response_result.data.data) == 1
|
||||
assert response_result.data.data[0].id == 1
|
||||
assert response_result.data.data[0].name == 'Cat'
|
||||
|
||||
def test_get_pets_invalid_response(self, spec, response_validator):
|
||||
host_url = 'http://petstore.swagger.io/v1'
|
||||
path_pattern = '/v1/pets'
|
||||
query_params = {
|
||||
'limit': '20',
|
||||
}
|
||||
|
||||
request = MockRequest(
|
||||
host_url, 'GET', '/pets',
|
||||
path_pattern=path_pattern, args=query_params,
|
||||
)
|
||||
|
||||
parameters = request.get_parameters(spec)
|
||||
body = request.get_body(spec)
|
||||
|
||||
assert parameters == {
|
||||
'query': {
|
||||
'limit': 20,
|
||||
'page': 1,
|
||||
'search': '',
|
||||
}
|
||||
}
|
||||
assert body is None
|
||||
|
||||
data_json = {
|
||||
'data': [
|
||||
{
|
||||
'id': 1,
|
||||
'name': {
|
||||
'first_name': 'Cat',
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
data = json.dumps(data_json)
|
||||
response = MockResponse(data)
|
||||
|
||||
response_result = response_validator.validate(request, response)
|
||||
|
||||
assert response_result.errors == [
|
||||
InvalidMediaTypeValue(
|
||||
original_exception=InvalidSchemaProperty(
|
||||
property_name='data',
|
||||
original_exception=InvalidSchemaProperty(
|
||||
property_name='name',
|
||||
original_exception=InvalidSchemaValue(
|
||||
msg="Value {value} is not of type {type}",
|
||||
type=SchemaType.STRING,
|
||||
value={'first_name': 'Cat'},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
assert response_result.data is None
|
||||
|
||||
def test_get_pets_ids_param(self, spec, response_validator):
|
||||
host_url = 'http://petstore.swagger.io/v1'
|
||||
path_pattern = '/v1/pets'
|
||||
|
@ -419,7 +519,7 @@ class TestPetstore(object):
|
|||
data_json = {
|
||||
'name': pet_name,
|
||||
'tag': pet_tag,
|
||||
'position': '2',
|
||||
'position': 2,
|
||||
'address': {
|
||||
'street': pet_street,
|
||||
'city': pet_city,
|
||||
|
@ -479,7 +579,7 @@ class TestPetstore(object):
|
|||
data_json = {
|
||||
'name': pet_name,
|
||||
'tag': pet_tag,
|
||||
'position': '2',
|
||||
'position': 2,
|
||||
'address': {
|
||||
'street': pet_street,
|
||||
'city': pet_city,
|
||||
|
@ -535,11 +635,11 @@ class TestPetstore(object):
|
|||
pet_tag = 'cats'
|
||||
pet_street = 'Piekna'
|
||||
pet_city = 'Warsaw'
|
||||
pet_healthy = 'false'
|
||||
pet_healthy = False
|
||||
data_json = {
|
||||
'name': pet_name,
|
||||
'tag': pet_tag,
|
||||
'position': '2',
|
||||
'position': 2,
|
||||
'address': {
|
||||
'street': pet_street,
|
||||
'city': pet_city,
|
||||
|
|
|
@ -155,7 +155,7 @@ class TestRequestValidator(object):
|
|||
data_json = {
|
||||
'name': pet_name,
|
||||
'tag': pet_tag,
|
||||
'position': '2',
|
||||
'position': 2,
|
||||
'address': {
|
||||
'street': pet_street,
|
||||
'city': pet_city,
|
||||
|
|
|
@ -48,6 +48,13 @@ class TestSchemaUnmarshal(object):
|
|||
|
||||
assert result == value
|
||||
|
||||
def test_string_float_invalid(self):
|
||||
schema = Schema('string')
|
||||
value = 1.23
|
||||
|
||||
with pytest.raises(InvalidSchemaValue):
|
||||
schema.unmarshal(value)
|
||||
|
||||
def test_string_none(self):
|
||||
schema = Schema('string')
|
||||
value = None
|
||||
|
@ -117,7 +124,7 @@ class TestSchemaUnmarshal(object):
|
|||
value = 'x'
|
||||
|
||||
with mock.patch.dict(
|
||||
Schema.STRING_FORMAT_CAST_CALLABLE_GETTER,
|
||||
Schema.STRING_FORMAT_CALLABLE_GETTER,
|
||||
{custom_format: mock.Mock(side_effect=ValueError())},
|
||||
), pytest.raises(
|
||||
InvalidSchemaValue, message='Failed to format value'
|
||||
|
@ -126,12 +133,19 @@ class TestSchemaUnmarshal(object):
|
|||
|
||||
def test_integer_valid(self):
|
||||
schema = Schema('integer')
|
||||
value = '123'
|
||||
value = 123
|
||||
|
||||
result = schema.unmarshal(value)
|
||||
|
||||
assert result == int(value)
|
||||
|
||||
def test_integer_string_invalid(self):
|
||||
schema = Schema('integer')
|
||||
value = '123'
|
||||
|
||||
with pytest.raises(InvalidSchemaValue):
|
||||
schema.unmarshal(value)
|
||||
|
||||
def test_integer_enum_invalid(self):
|
||||
schema = Schema('integer', enum=[1, 2, 3])
|
||||
value = '123'
|
||||
|
@ -141,12 +155,19 @@ class TestSchemaUnmarshal(object):
|
|||
|
||||
def test_integer_enum(self):
|
||||
schema = Schema('integer', enum=[1, 2, 3])
|
||||
value = '2'
|
||||
value = 2
|
||||
|
||||
result = schema.unmarshal(value)
|
||||
|
||||
assert result == int(value)
|
||||
|
||||
def test_integer_enum_string_invalid(self):
|
||||
schema = Schema('integer', enum=[1, 2, 3])
|
||||
value = '2'
|
||||
|
||||
with pytest.raises(InvalidSchemaValue):
|
||||
schema.unmarshal(value)
|
||||
|
||||
def test_integer_default(self):
|
||||
default_value = '123'
|
||||
schema = Schema('integer', default=default_value)
|
||||
|
@ -171,6 +192,65 @@ class TestSchemaUnmarshal(object):
|
|||
with pytest.raises(InvalidSchemaValue):
|
||||
schema.unmarshal(value)
|
||||
|
||||
def test_array_valid(self):
|
||||
schema = Schema('array', items=Schema('integer'))
|
||||
value = [1, 2, 3]
|
||||
|
||||
result = schema.unmarshal(value)
|
||||
|
||||
assert result == value
|
||||
|
||||
def test_array_of_string_string_invalid(self):
|
||||
schema = Schema('array', items=Schema('string'))
|
||||
value = '123'
|
||||
|
||||
with pytest.raises(InvalidSchemaValue):
|
||||
schema.unmarshal(value)
|
||||
|
||||
def test_array_of_integer_string_invalid(self):
|
||||
schema = Schema('array', items=Schema('integer'))
|
||||
value = '123'
|
||||
|
||||
with pytest.raises(InvalidSchemaValue):
|
||||
schema.unmarshal(value)
|
||||
|
||||
def test_boolean_valid(self):
|
||||
schema = Schema('boolean')
|
||||
value = True
|
||||
|
||||
result = schema.unmarshal(value)
|
||||
|
||||
assert result == value
|
||||
|
||||
def test_boolean_string_invalid(self):
|
||||
schema = Schema('boolean')
|
||||
value = 'True'
|
||||
|
||||
with pytest.raises(InvalidSchemaValue):
|
||||
schema.unmarshal(value)
|
||||
|
||||
def test_number_valid(self):
|
||||
schema = Schema('number')
|
||||
value = 1.23
|
||||
|
||||
result = schema.unmarshal(value)
|
||||
|
||||
assert result == value
|
||||
|
||||
def test_number_string_invalid(self):
|
||||
schema = Schema('number')
|
||||
value = '1.23'
|
||||
|
||||
with pytest.raises(InvalidSchemaValue):
|
||||
schema.unmarshal(value)
|
||||
|
||||
def test_number_int_invalid(self):
|
||||
schema = Schema('number')
|
||||
value = 1
|
||||
|
||||
with pytest.raises(InvalidSchemaValue):
|
||||
schema.unmarshal(value)
|
||||
|
||||
|
||||
class TestSchemaValidate(object):
|
||||
|
||||
|
@ -763,3 +843,65 @@ class TestSchemaValidate(object):
|
|||
|
||||
with pytest.raises(Exception):
|
||||
schema.validate(value)
|
||||
|
||||
@pytest.mark.parametrize('value', [
|
||||
Model({
|
||||
'someint': 123,
|
||||
}),
|
||||
Model({
|
||||
'somestr': u('content'),
|
||||
}),
|
||||
Model({
|
||||
'somestr': u('content'),
|
||||
'someint': 123,
|
||||
}),
|
||||
])
|
||||
def test_object_with_properties(self, value):
|
||||
schema = Schema(
|
||||
'object',
|
||||
properties={
|
||||
'somestr': Schema('string'),
|
||||
'someint': Schema('integer'),
|
||||
},
|
||||
)
|
||||
|
||||
result = schema.validate(value)
|
||||
|
||||
assert result == value
|
||||
|
||||
@pytest.mark.parametrize('value', [
|
||||
Model({
|
||||
'somestr': Model(),
|
||||
'someint': 123,
|
||||
}),
|
||||
Model({
|
||||
'somestr': {},
|
||||
'someint': 123,
|
||||
}),
|
||||
Model({
|
||||
'somestr': [
|
||||
'content1', 'content2'
|
||||
],
|
||||
'someint': 123,
|
||||
}),
|
||||
Model({
|
||||
'somestr': 123,
|
||||
'someint': 123,
|
||||
}),
|
||||
Model({
|
||||
'somestr': 'content',
|
||||
'someint': 123,
|
||||
'not_in_scheme_prop': 123,
|
||||
}),
|
||||
])
|
||||
def test_object_with_invalid_properties(self, value):
|
||||
schema = Schema(
|
||||
'object',
|
||||
properties={
|
||||
'somestr': Schema('string'),
|
||||
'someint': Schema('integer'),
|
||||
},
|
||||
)
|
||||
|
||||
with pytest.raises(Exception):
|
||||
schema.validate(value)
|
||||
|
|
Loading…
Reference in a new issue