Skip to content

Raw value type strict validation #123

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion openapi_core/schema/parameters/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,11 @@ def unmarshal(self, value, custom_formatters=None):
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)

Expand Down
59 changes: 43 additions & 16 deletions openapi_core/schema/schemas/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -148,27 +145,31 @@ def get_all_required_properties_names(self):

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),
})

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
Expand All @@ -180,12 +181,12 @@ def cast(self, value, custom_formatters=None):
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
Expand All @@ -196,7 +197,10 @@ def unmarshal(self, value, custom_formatters=None):

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:
Expand All @@ -216,7 +220,25 @@ def _unmarshal_string(self, value, custom_formatters=None):
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, )):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a case where I return a number with a value of 3
3 contrary to 3.0 is not recognized as a 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,
Expand All @@ -232,16 +254,21 @@ def _unmarshal_any(self, value, custom_formatters=None):

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)

Expand Down Expand Up @@ -270,7 +297,7 @@ def _unmarshal_object(self, value, model_factory=None,
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()
Expand Down
34 changes: 18 additions & 16 deletions tests/integration/data/v3.0/petstore.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
110 changes: 105 additions & 5 deletions tests/integration/test_petstore.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
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
Expand Down Expand Up @@ -234,6 +235,105 @@ def test_get_pets(self, spec, response_validator):
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'
Expand Down Expand Up @@ -419,7 +519,7 @@ def test_post_birds(self, spec, spec_dict):
data_json = {
'name': pet_name,
'tag': pet_tag,
'position': '2',
'position': 2,
'address': {
'street': pet_street,
'city': pet_city,
Expand Down Expand Up @@ -479,7 +579,7 @@ def test_post_cats(self, spec, spec_dict):
data_json = {
'name': pet_name,
'tag': pet_tag,
'position': '2',
'position': 2,
'address': {
'street': pet_street,
'city': pet_city,
Expand Down Expand Up @@ -535,11 +635,11 @@ def test_post_cats_boolean_string(self, spec, spec_dict):
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,
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def test_post_pets(self, validator, spec_dict):
data_json = {
'name': pet_name,
'tag': pet_tag,
'position': '2',
'position': 2,
'address': {
'street': pet_street,
'city': pet_city,
Expand Down
Loading