Merge pull request #84 from p1c2u/feature/object-validation

Object validation
This commit is contained in:
A 2018-08-22 10:18:11 +01:00 committed by GitHub
commit 7d1f568bd4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 276 additions and 44 deletions

View file

@ -1,12 +1,25 @@
"""OpenAPI X-Model extension factories module"""
from openapi_core.extensions.models.models import BaseModel
from openapi_core.extensions.models.models import Model
class ModelClassFactory(object):
base_class = Model
def create(self, name):
return type(name, (self.base_class, ), {})
class ModelFactory(object):
def create(self, properties, name=None):
model = BaseModel
if name is not None:
model = type(name, (BaseModel, ), {})
def __init__(self, model_class_factory=None):
self.model_class_factory = model_class_factory or ModelClassFactory()
return model(**properties)
def create(self, properties, name=None):
name = name or 'Model'
model_class = self._create_class(name)
return model_class(properties)
def _create_class(self, name):
return self.model_class_factory.create(name)

View file

@ -1,17 +1,26 @@
"""OpenAPI X-Model extension models module"""
class BaseModel(dict):
class BaseModel(object):
"""Base class for OpenAPI X-Model."""
def __getattr__(self, attr_name):
"""Only search through properties if attribute not found normally.
:type attr_name: str
"""
try:
return self[attr_name]
except KeyError:
raise AttributeError(
'type object {0!r} has no attribute {1!r}'
.format(type(self).__name__, attr_name)
)
@property
def __dict__(self):
raise NotImplementedError
class Model(BaseModel):
"""Model class for OpenAPI X-Model."""
def __init__(self, properties=None):
self.__properties = properties or {}
@property
def __dict__(self):
return self.__properties
def __getattr__(self, name):
if name not in self.__properties:
raise AttributeError
return self.__properties[name]

View file

@ -32,14 +32,14 @@ class Schema(object):
SchemaFormat.DATE.value: format_date,
})
VALIDATOR_CALLABLE_GETTER = {
TYPE_VALIDATOR_CALLABLE_GETTER = {
None: lambda x: x,
SchemaType.BOOLEAN: TypeValidator(bool),
SchemaType.INTEGER: TypeValidator(integer_types, exclude=bool),
SchemaType.NUMBER: TypeValidator(integer_types, float, exclude=bool),
SchemaType.STRING: TypeValidator(binary_type, text_type),
SchemaType.ARRAY: TypeValidator(list, tuple),
SchemaType.OBJECT: AttributeValidator('__class__'),
SchemaType.OBJECT: AttributeValidator('__dict__'),
}
def __init__(
@ -170,10 +170,12 @@ class Schema(object):
def _unmarshal_collection(self, value):
return list(map(self.items.unmarshal, value))
def _unmarshal_object(self, value):
def _unmarshal_object(self, value, model_factory=None):
if not isinstance(value, (dict, )):
raise InvalidSchemaValue(
"Value of {0} not an object".format(value))
"Value of {0} not a dict".format(value))
model_factory = model_factory or ModelFactory()
if self.one_of:
properties = None
@ -197,7 +199,7 @@ class Schema(object):
else:
properties = self._unmarshal_properties(value)
return ModelFactory().create(properties, name=self.model)
return model_factory.create(properties, name=self.model)
def _unmarshal_properties(self, value, one_of_schema=None):
all_props = self.get_all_properties()
@ -234,20 +236,99 @@ class Schema(object):
continue
prop_value = prop.default
properties[prop_name] = prop.unmarshal(prop_value)
self._validate_properties(properties, one_of_schema=one_of_schema)
return properties
def get_validator_mapping(self):
mapping = {
SchemaType.OBJECT: self._validate_object,
}
return defaultdict(lambda: lambda x: x, mapping)
def validate(self, value):
if value is None:
if not self.nullable:
raise InvalidSchemaValue("Null value for non-nullable schema")
return self.default
return
validator = self.VALIDATOR_CALLABLE_GETTER[self.type]
if not validator(value):
# type validation
type_validator_callable = self.TYPE_VALIDATOR_CALLABLE_GETTER[
self.type]
if not type_validator_callable(value):
raise InvalidSchemaValue(
"Value of {0} not valid type of {1}".format(
value, self.type.value)
)
# structure validation
validator_mapping = self.get_validator_mapping()
validator_callable = validator_mapping[self.type]
validator_callable(value)
return value
def _validate_object(self, value):
properties = value.__dict__
if self.one_of:
valid_one_of_schema = None
for one_of_schema in self.one_of:
try:
self._validate_properties(properties, one_of_schema)
except OpenAPISchemaError:
pass
else:
if valid_one_of_schema is not None:
raise MultipleOneOfSchema(
"Exactly one schema should be valid,"
"multiple found")
valid_one_of_schema = True
if valid_one_of_schema is None:
raise NoOneOfSchema(
"Exactly one valid schema should be valid, None found.")
else:
self._validate_properties(properties)
return True
def _validate_properties(self, value, one_of_schema=None):
all_props = self.get_all_properties()
all_props_names = self.get_all_properties_names()
all_req_props_names = self.get_all_required_properties_names()
if one_of_schema is not None:
all_props.update(one_of_schema.get_all_properties())
all_props_names |= one_of_schema.\
get_all_properties_names()
all_req_props_names |= one_of_schema.\
get_all_required_properties_names()
value_props_names = value.keys()
extra_props = set(value_props_names) - set(all_props_names)
if extra_props and self.additional_properties is None:
raise UndefinedSchemaProperty(
"Undefined properties in schema: {0}".format(extra_props))
for prop_name in extra_props:
prop_value = value[prop_name]
self.additional_properties.validate(
prop_value)
for prop_name, prop in iteritems(all_props):
try:
prop_value = value[prop_name]
except KeyError:
if prop_name in all_req_props_names:
raise MissingSchemaProperty(
"Missing schema property {0}".format(prop_name))
if not prop.nullable and not prop.default:
continue
prop_value = prop.default
prop.validate(prop_value)
return True

View file

@ -2,6 +2,7 @@ import json
import pytest
from six import iteritems
from openapi_core.extensions.models.models import BaseModel
from openapi_core.schema.media_types.exceptions import (
InvalidContentType, InvalidMediaTypeValue,
)
@ -221,7 +222,8 @@ class TestPetstore(object):
response_result = response_validator.validate(request, response)
assert response_result.errors == []
assert response_result.data == data_json
assert isinstance(response_result.data, BaseModel)
assert response_result.data.data == []
def test_get_pets_ids_param(self, spec, response_validator):
host_url = 'http://petstore.swagger.io/v1'
@ -258,7 +260,8 @@ class TestPetstore(object):
response_result = response_validator.validate(request, response)
assert response_result.errors == []
assert response_result.data == data_json
assert isinstance(response_result.data, BaseModel)
assert response_result.data.data == []
def test_get_pets_tags_param(self, spec, response_validator):
host_url = 'http://petstore.swagger.io/v1'
@ -295,7 +298,8 @@ class TestPetstore(object):
response_result = response_validator.validate(request, response)
assert response_result.errors == []
assert response_result.data == data_json
assert isinstance(response_result.data, BaseModel)
assert response_result.data.data == []
def test_get_pets_parameter_deserialization_error(self, spec):
host_url = 'http://petstore.swagger.io/v1'
@ -810,10 +814,12 @@ class TestPetstore(object):
assert body is None
data_id = 1
data_name = 'test'
data_json = {
'data': {
'id': 1,
'name': 'test',
'id': data_id,
'name': data_name,
},
}
data = json.dumps(data_json)
@ -822,7 +828,10 @@ class TestPetstore(object):
response_result = response_validator.validate(request, response)
assert response_result.errors == []
assert response_result.data == data_json
assert isinstance(response_result.data, BaseModel)
assert isinstance(response_result.data.data, BaseModel)
assert response_result.data.data.id == data_id
assert response_result.data.data.name == data_name
def test_get_pet_not_found(self, spec, response_validator):
host_url = 'http://petstore.swagger.io/v1'
@ -847,10 +856,13 @@ class TestPetstore(object):
assert body is None
code = 404
message = 'Not found'
rootCause = 'Pet not found'
data_json = {
'code': 404,
'message': 'Not found',
'rootCause': 'Pet not found',
'message': message,
'rootCause': rootCause,
}
data = json.dumps(data_json)
response = MockResponse(data, status_code=404)
@ -858,7 +870,10 @@ class TestPetstore(object):
response_result = response_validator.validate(request, response)
assert response_result.errors == []
assert response_result.data == data_json
assert isinstance(response_result.data, BaseModel)
assert response_result.data.code == code
assert response_result.data.message == message
assert response_result.data.rootCause == rootCause
def test_get_pet_wildcard(self, spec, response_validator):
host_url = 'http://petstore.swagger.io/v1'
@ -993,13 +1008,18 @@ class TestPetstore(object):
body = request.get_body(spec)
assert parameters == {}
assert body == data_json
assert isinstance(body, BaseModel)
assert body.name == pet_name
code = 400
message = 'Bad request'
rootCause = 'Tag already exist'
additionalinfo = 'Tag Dog already exist'
data_json = {
'code': 400,
'message': 'Bad request',
'rootCause': 'Tag already exist',
'additionalinfo': 'Tag Dog already exist',
'code': code,
'message': message,
'rootCause': rootCause,
'additionalinfo': additionalinfo,
}
data = json.dumps(data_json)
response = MockResponse(data, status_code=404)
@ -1007,4 +1027,8 @@ class TestPetstore(object):
response_result = response_validator.validate(request, response)
assert response_result.errors == []
assert response_result.data == data_json
assert isinstance(response_result.data, BaseModel)
assert response_result.data.code == code
assert response_result.data.message == message
assert response_result.data.rootCause == rootCause
assert response_result.data.additionalinfo == additionalinfo

View file

@ -4,6 +4,7 @@ import pytest
from openapi_core.schema.media_types.exceptions import (
InvalidContentType, InvalidMediaTypeValue,
)
from openapi_core.extensions.models.models import BaseModel
from openapi_core.schema.operations.exceptions import InvalidOperation
from openapi_core.schema.parameters.exceptions import MissingRequiredParameter
from openapi_core.schema.request_bodies.exceptions import MissingRequestBody
@ -327,5 +328,8 @@ class TestResponseValidator(object):
result = validator.validate(request, response)
assert result.errors == []
assert result.data == response_json
assert isinstance(result.data, BaseModel)
assert len(result.data.data) == 1
assert result.data.data[0].id == 1
assert result.data.data[0].name == 'Sparky'
assert result.headers == {}

View file

@ -0,0 +1,44 @@
import pytest
from openapi_core.extensions.models.models import BaseModel, Model
class TestBaseModelDict(object):
def test_not_implemented(self):
model = BaseModel()
with pytest.raises(NotImplementedError):
model.__dict__
class TestModelDict(object):
def test_dict_empty(self):
model = Model()
result = model.__dict__
assert result == {}
def test_dict(self):
properties = {
'prop1': 'value1',
'prop2': 'value2',
}
model = Model(properties)
result = model.__dict__
assert result == properties
def test_attribute(self):
prop_value = 'value1'
properties = {
'prop1': prop_value,
}
model = Model(properties)
result = model.prop1
assert result == prop_value

View file

@ -3,7 +3,10 @@ import datetime
import mock
import pytest
from openapi_core.schema.schemas.exceptions import InvalidSchemaValue
from openapi_core.extensions.models.models import Model
from openapi_core.schema.schemas.exceptions import (
InvalidSchemaValue, MultipleOneOfSchema, NoOneOfSchema,
)
from openapi_core.schema.schemas.models import Schema
@ -254,3 +257,57 @@ class TestSchemaValidate(object):
with pytest.raises(InvalidSchemaValue):
schema.validate(value)
@pytest.mark.parametrize('value', ['true', False, 1, 3.14, [1, 3]])
def test_object_not_an_object(self, value):
schema = Schema('object')
with pytest.raises(InvalidSchemaValue):
schema.validate(value)
@pytest.mark.parametrize('value', [Model(), ])
def test_object_multiple_one_of(self, value):
one_of = [
Schema('object'), Schema('object'),
]
schema = Schema('object', one_of=one_of)
with pytest.raises(MultipleOneOfSchema):
schema.validate(value)
@pytest.mark.parametrize('value', [Model(), ])
def test_object_defferent_type_one_of(self, value):
one_of = [
Schema('integer'), Schema('string'),
]
schema = Schema('object', one_of=one_of)
with pytest.raises(MultipleOneOfSchema):
schema.validate(value)
@pytest.mark.parametrize('value', [Model(), ])
def test_object_no_one_of(self, value):
one_of = [
Schema(
'object',
properties={'test1': Schema('string')},
required=['test1', ],
),
Schema(
'object',
properties={'test2': Schema('string')},
required=['test2', ],
),
]
schema = Schema('object', one_of=one_of)
with pytest.raises(NoOneOfSchema):
schema.validate(value)
@pytest.mark.parametrize('value', [Model(), ])
def test_object_default_property(self, value):
schema = Schema('object', default='value1')
result = schema.validate(value)
assert result == value