Skip to content

Commit 7d1f568

Browse files
authored
Merge pull request #84 from p1c2u/feature/object-validation
Object validation
2 parents 17855ae + 3aaa3ce commit 7d1f568

File tree

7 files changed

+275
-43
lines changed

7 files changed

+275
-43
lines changed
Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
11
"""OpenAPI X-Model extension factories module"""
2-
from openapi_core.extensions.models.models import BaseModel
2+
from openapi_core.extensions.models.models import Model
3+
4+
5+
class ModelClassFactory(object):
6+
7+
base_class = Model
8+
9+
def create(self, name):
10+
return type(name, (self.base_class, ), {})
311

412

513
class ModelFactory(object):
614

15+
def __init__(self, model_class_factory=None):
16+
self.model_class_factory = model_class_factory or ModelClassFactory()
17+
718
def create(self, properties, name=None):
8-
model = BaseModel
9-
if name is not None:
10-
model = type(name, (BaseModel, ), {})
19+
name = name or 'Model'
20+
21+
model_class = self._create_class(name)
22+
return model_class(properties)
1123

12-
return model(**properties)
24+
def _create_class(self, name):
25+
return self.model_class_factory.create(name)
Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
11
"""OpenAPI X-Model extension models module"""
22

33

4-
class BaseModel(dict):
4+
class BaseModel(object):
55
"""Base class for OpenAPI X-Model."""
66

7-
def __getattr__(self, attr_name):
8-
"""Only search through properties if attribute not found normally.
9-
:type attr_name: str
10-
"""
11-
try:
12-
return self[attr_name]
13-
except KeyError:
14-
raise AttributeError(
15-
'type object {0!r} has no attribute {1!r}'
16-
.format(type(self).__name__, attr_name)
17-
)
7+
@property
8+
def __dict__(self):
9+
raise NotImplementedError
10+
11+
12+
class Model(BaseModel):
13+
"""Model class for OpenAPI X-Model."""
14+
15+
def __init__(self, properties=None):
16+
self.__properties = properties or {}
17+
18+
@property
19+
def __dict__(self):
20+
return self.__properties
21+
22+
def __getattr__(self, name):
23+
if name not in self.__properties:
24+
raise AttributeError
25+
26+
return self.__properties[name]

openapi_core/schema/schemas/models.py

Lines changed: 90 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,14 @@ class Schema(object):
3232
SchemaFormat.DATE.value: format_date,
3333
})
3434

35-
VALIDATOR_CALLABLE_GETTER = {
35+
TYPE_VALIDATOR_CALLABLE_GETTER = {
3636
None: lambda x: x,
3737
SchemaType.BOOLEAN: TypeValidator(bool),
3838
SchemaType.INTEGER: TypeValidator(integer_types, exclude=bool),
3939
SchemaType.NUMBER: TypeValidator(integer_types, float, exclude=bool),
4040
SchemaType.STRING: TypeValidator(binary_type, text_type),
4141
SchemaType.ARRAY: TypeValidator(list, tuple),
42-
SchemaType.OBJECT: AttributeValidator('__class__'),
42+
SchemaType.OBJECT: AttributeValidator('__dict__'),
4343
}
4444

4545
def __init__(
@@ -170,10 +170,12 @@ def _unmarshal_string(self, value):
170170
def _unmarshal_collection(self, value):
171171
return list(map(self.items.unmarshal, value))
172172

173-
def _unmarshal_object(self, value):
173+
def _unmarshal_object(self, value, model_factory=None):
174174
if not isinstance(value, (dict, )):
175175
raise InvalidSchemaValue(
176-
"Value of {0} not an object".format(value))
176+
"Value of {0} not a dict".format(value))
177+
178+
model_factory = model_factory or ModelFactory()
177179

178180
if self.one_of:
179181
properties = None
@@ -197,7 +199,7 @@ def _unmarshal_object(self, value):
197199
else:
198200
properties = self._unmarshal_properties(value)
199201

200-
return ModelFactory().create(properties, name=self.model)
202+
return model_factory.create(properties, name=self.model)
201203

202204
def _unmarshal_properties(self, value, one_of_schema=None):
203205
all_props = self.get_all_properties()
@@ -234,20 +236,99 @@ def _unmarshal_properties(self, value, one_of_schema=None):
234236
continue
235237
prop_value = prop.default
236238
properties[prop_name] = prop.unmarshal(prop_value)
239+
240+
self._validate_properties(properties, one_of_schema=one_of_schema)
241+
237242
return properties
238243

244+
def get_validator_mapping(self):
245+
mapping = {
246+
SchemaType.OBJECT: self._validate_object,
247+
}
248+
249+
return defaultdict(lambda: lambda x: x, mapping)
250+
239251
def validate(self, value):
240252
if value is None:
241253
if not self.nullable:
242254
raise InvalidSchemaValue("Null value for non-nullable schema")
243-
return self.default
244-
245-
validator = self.VALIDATOR_CALLABLE_GETTER[self.type]
255+
return
246256

247-
if not validator(value):
257+
# type validation
258+
type_validator_callable = self.TYPE_VALIDATOR_CALLABLE_GETTER[
259+
self.type]
260+
if not type_validator_callable(value):
248261
raise InvalidSchemaValue(
249262
"Value of {0} not valid type of {1}".format(
250263
value, self.type.value)
251264
)
252265

266+
# structure validation
267+
validator_mapping = self.get_validator_mapping()
268+
validator_callable = validator_mapping[self.type]
269+
validator_callable(value)
270+
253271
return value
272+
273+
def _validate_object(self, value):
274+
properties = value.__dict__
275+
276+
if self.one_of:
277+
valid_one_of_schema = None
278+
for one_of_schema in self.one_of:
279+
try:
280+
self._validate_properties(properties, one_of_schema)
281+
except OpenAPISchemaError:
282+
pass
283+
else:
284+
if valid_one_of_schema is not None:
285+
raise MultipleOneOfSchema(
286+
"Exactly one schema should be valid,"
287+
"multiple found")
288+
valid_one_of_schema = True
289+
290+
if valid_one_of_schema is None:
291+
raise NoOneOfSchema(
292+
"Exactly one valid schema should be valid, None found.")
293+
294+
else:
295+
self._validate_properties(properties)
296+
297+
return True
298+
299+
def _validate_properties(self, value, one_of_schema=None):
300+
all_props = self.get_all_properties()
301+
all_props_names = self.get_all_properties_names()
302+
all_req_props_names = self.get_all_required_properties_names()
303+
304+
if one_of_schema is not None:
305+
all_props.update(one_of_schema.get_all_properties())
306+
all_props_names |= one_of_schema.\
307+
get_all_properties_names()
308+
all_req_props_names |= one_of_schema.\
309+
get_all_required_properties_names()
310+
311+
value_props_names = value.keys()
312+
extra_props = set(value_props_names) - set(all_props_names)
313+
if extra_props and self.additional_properties is None:
314+
raise UndefinedSchemaProperty(
315+
"Undefined properties in schema: {0}".format(extra_props))
316+
317+
for prop_name in extra_props:
318+
prop_value = value[prop_name]
319+
self.additional_properties.validate(
320+
prop_value)
321+
322+
for prop_name, prop in iteritems(all_props):
323+
try:
324+
prop_value = value[prop_name]
325+
except KeyError:
326+
if prop_name in all_req_props_names:
327+
raise MissingSchemaProperty(
328+
"Missing schema property {0}".format(prop_name))
329+
if not prop.nullable and not prop.default:
330+
continue
331+
prop_value = prop.default
332+
prop.validate(prop_value)
333+
334+
return True

tests/integration/test_petstore.py

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import pytest
33
from six import iteritems
44

5+
from openapi_core.extensions.models.models import BaseModel
56
from openapi_core.schema.media_types.exceptions import (
67
InvalidContentType, InvalidMediaTypeValue,
78
)
@@ -221,7 +222,8 @@ def test_get_pets(self, spec, response_validator):
221222
response_result = response_validator.validate(request, response)
222223

223224
assert response_result.errors == []
224-
assert response_result.data == data_json
225+
assert isinstance(response_result.data, BaseModel)
226+
assert response_result.data.data == []
225227

226228
def test_get_pets_ids_param(self, spec, response_validator):
227229
host_url = 'http://petstore.swagger.io/v1'
@@ -258,7 +260,8 @@ def test_get_pets_ids_param(self, spec, response_validator):
258260
response_result = response_validator.validate(request, response)
259261

260262
assert response_result.errors == []
261-
assert response_result.data == data_json
263+
assert isinstance(response_result.data, BaseModel)
264+
assert response_result.data.data == []
262265

263266
def test_get_pets_tags_param(self, spec, response_validator):
264267
host_url = 'http://petstore.swagger.io/v1'
@@ -295,7 +298,8 @@ def test_get_pets_tags_param(self, spec, response_validator):
295298
response_result = response_validator.validate(request, response)
296299

297300
assert response_result.errors == []
298-
assert response_result.data == data_json
301+
assert isinstance(response_result.data, BaseModel)
302+
assert response_result.data.data == []
299303

300304
def test_get_pets_parameter_deserialization_error(self, spec):
301305
host_url = 'http://petstore.swagger.io/v1'
@@ -810,10 +814,12 @@ def test_get_pet(self, spec, response_validator):
810814

811815
assert body is None
812816

817+
data_id = 1
818+
data_name = 'test'
813819
data_json = {
814820
'data': {
815-
'id': 1,
816-
'name': 'test',
821+
'id': data_id,
822+
'name': data_name,
817823
},
818824
}
819825
data = json.dumps(data_json)
@@ -822,7 +828,10 @@ def test_get_pet(self, spec, response_validator):
822828
response_result = response_validator.validate(request, response)
823829

824830
assert response_result.errors == []
825-
assert response_result.data == data_json
831+
assert isinstance(response_result.data, BaseModel)
832+
assert isinstance(response_result.data.data, BaseModel)
833+
assert response_result.data.data.id == data_id
834+
assert response_result.data.data.name == data_name
826835

827836
def test_get_pet_not_found(self, spec, response_validator):
828837
host_url = 'http://petstore.swagger.io/v1'
@@ -847,18 +856,24 @@ def test_get_pet_not_found(self, spec, response_validator):
847856

848857
assert body is None
849858

859+
code = 404
860+
message = 'Not found'
861+
rootCause = 'Pet not found'
850862
data_json = {
851863
'code': 404,
852-
'message': 'Not found',
853-
'rootCause': 'Pet not found',
864+
'message': message,
865+
'rootCause': rootCause,
854866
}
855867
data = json.dumps(data_json)
856868
response = MockResponse(data, status_code=404)
857869

858870
response_result = response_validator.validate(request, response)
859871

860872
assert response_result.errors == []
861-
assert response_result.data == data_json
873+
assert isinstance(response_result.data, BaseModel)
874+
assert response_result.data.code == code
875+
assert response_result.data.message == message
876+
assert response_result.data.rootCause == rootCause
862877

863878
def test_get_pet_wildcard(self, spec, response_validator):
864879
host_url = 'http://petstore.swagger.io/v1'
@@ -993,18 +1008,27 @@ def test_post_tags_additional_properties(
9931008
body = request.get_body(spec)
9941009

9951010
assert parameters == {}
996-
assert body == data_json
1011+
assert isinstance(body, BaseModel)
1012+
assert body.name == pet_name
9971013

1014+
code = 400
1015+
message = 'Bad request'
1016+
rootCause = 'Tag already exist'
1017+
additionalinfo = 'Tag Dog already exist'
9981018
data_json = {
999-
'code': 400,
1000-
'message': 'Bad request',
1001-
'rootCause': 'Tag already exist',
1002-
'additionalinfo': 'Tag Dog already exist',
1019+
'code': code,
1020+
'message': message,
1021+
'rootCause': rootCause,
1022+
'additionalinfo': additionalinfo,
10031023
}
10041024
data = json.dumps(data_json)
10051025
response = MockResponse(data, status_code=404)
10061026

10071027
response_result = response_validator.validate(request, response)
10081028

10091029
assert response_result.errors == []
1010-
assert response_result.data == data_json
1030+
assert isinstance(response_result.data, BaseModel)
1031+
assert response_result.data.code == code
1032+
assert response_result.data.message == message
1033+
assert response_result.data.rootCause == rootCause
1034+
assert response_result.data.additionalinfo == additionalinfo

tests/integration/test_validators.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from openapi_core.schema.media_types.exceptions import (
55
InvalidContentType, InvalidMediaTypeValue,
66
)
7+
from openapi_core.extensions.models.models import BaseModel
78
from openapi_core.schema.operations.exceptions import InvalidOperation
89
from openapi_core.schema.parameters.exceptions import MissingRequiredParameter
910
from openapi_core.schema.request_bodies.exceptions import MissingRequestBody
@@ -327,5 +328,8 @@ def test_get_pets(self, validator):
327328
result = validator.validate(request, response)
328329

329330
assert result.errors == []
330-
assert result.data == response_json
331+
assert isinstance(result.data, BaseModel)
332+
assert len(result.data.data) == 1
333+
assert result.data.data[0].id == 1
334+
assert result.data.data[0].name == 'Sparky'
331335
assert result.headers == {}

0 commit comments

Comments
 (0)