Merge pull request #86 from p1c2u/feature/string-validation

String validation
This commit is contained in:
A 2018-08-22 14:01:21 +01:00 committed by GitHub
commit 4731504f32
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 148 additions and 18 deletions

View file

@ -1,6 +1,7 @@
"""OpenAPI core schemas models module""" """OpenAPI core schemas models module"""
import logging import logging
from collections import defaultdict from collections import defaultdict
from datetime import date, datetime
import warnings import warnings
from six import iteritems, integer_types, binary_type, text_type from six import iteritems, integer_types, binary_type, text_type
@ -11,7 +12,9 @@ from openapi_core.schema.schemas.exceptions import (
InvalidSchemaValue, UndefinedSchemaProperty, MissingSchemaProperty, InvalidSchemaValue, UndefinedSchemaProperty, MissingSchemaProperty,
OpenAPISchemaError, NoOneOfSchema, MultipleOneOfSchema, OpenAPISchemaError, NoOneOfSchema, MultipleOneOfSchema,
) )
from openapi_core.schema.schemas.util import forcebool, format_date from openapi_core.schema.schemas.util import (
forcebool, format_date, format_datetime,
)
from openapi_core.schema.schemas.validators import ( from openapi_core.schema.schemas.validators import (
TypeValidator, AttributeValidator, TypeValidator, AttributeValidator,
) )
@ -28,16 +31,27 @@ class Schema(object):
SchemaType.BOOLEAN: forcebool, SchemaType.BOOLEAN: forcebool,
} }
FORMAT_CALLABLE_GETTER = defaultdict(lambda: lambda x: x, { STRING_FORMAT_CAST_CALLABLE_GETTER = {
SchemaFormat.DATE.value: format_date, SchemaFormat.NONE: text_type,
}) SchemaFormat.DATE: format_date,
SchemaFormat.DATETIME: format_datetime,
SchemaFormat.BINARY: binary_type,
}
STRING_FORMAT_VALIDATOR_CALLABLE_GETTER = {
SchemaFormat.NONE: TypeValidator(text_type),
SchemaFormat.DATE: TypeValidator(date, exclude=datetime),
SchemaFormat.DATETIME: TypeValidator(datetime),
SchemaFormat.BINARY: TypeValidator(binary_type),
}
TYPE_VALIDATOR_CALLABLE_GETTER = { TYPE_VALIDATOR_CALLABLE_GETTER = {
None: lambda x: x, None: lambda x: True,
SchemaType.BOOLEAN: TypeValidator(bool), SchemaType.BOOLEAN: TypeValidator(bool),
SchemaType.INTEGER: TypeValidator(integer_types, exclude=bool), SchemaType.INTEGER: TypeValidator(integer_types, exclude=bool),
SchemaType.NUMBER: TypeValidator(integer_types, float, exclude=bool), SchemaType.NUMBER: TypeValidator(integer_types, float, exclude=bool),
SchemaType.STRING: TypeValidator(binary_type, text_type), SchemaType.STRING: TypeValidator(
text_type, date, datetime, binary_type),
SchemaType.ARRAY: TypeValidator(list, tuple), SchemaType.ARRAY: TypeValidator(list, tuple),
SchemaType.OBJECT: AttributeValidator('__dict__'), SchemaType.OBJECT: AttributeValidator('__dict__'),
} }
@ -158,7 +172,16 @@ class Schema(object):
return casted return casted
def _unmarshal_string(self, value): def _unmarshal_string(self, value):
formatter = self.FORMAT_CALLABLE_GETTER[self.format] try:
schema_format = SchemaFormat(self.format)
except ValueError:
# @todo: implement custom format unmarshalling support
raise OpenAPISchemaError(
"Unsupported {0} format unmarshalling".format(self.format)
)
else:
formatter = self.STRING_FORMAT_CAST_CALLABLE_GETTER[schema_format]
try: try:
return formatter(value) return formatter(value)
except ValueError: except ValueError:
@ -244,6 +267,7 @@ class Schema(object):
def get_validator_mapping(self): def get_validator_mapping(self):
mapping = { mapping = {
SchemaType.ARRAY: self._validate_collection, SchemaType.ARRAY: self._validate_collection,
SchemaType.STRING: self._validate_string,
SchemaType.OBJECT: self._validate_object, SchemaType.OBJECT: self._validate_object,
} }
@ -277,6 +301,26 @@ class Schema(object):
return list(map(self.items.validate, value)) return list(map(self.items.validate, value))
def _validate_string(self, value):
try:
schema_format = SchemaFormat(self.format)
except ValueError:
# @todo: implement custom format validation support
raise OpenAPISchemaError(
"Unsupported {0} format validation".format(self.format)
)
else:
format_validator_callable =\
self.STRING_FORMAT_VALIDATOR_CALLABLE_GETTER[schema_format]
if not format_validator_callable(value):
raise InvalidSchemaValue(
"Value of {0} not valid format of {1}".format(
value, self.format)
)
return True
def _validate_object(self, value): def _validate_object(self, value):
properties = value.__dict__ properties = value.__dict__

View file

@ -18,3 +18,7 @@ def dicthash(d):
def format_date(value): def format_date(value):
return datetime.datetime.strptime(value, '%Y-%m-%d').date() return datetime.datetime.strptime(value, '%Y-%m-%d').date()
def format_datetime(value):
return datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%S')

View file

@ -286,7 +286,6 @@ components:
properties: properties:
name: name:
type: string type: string
format: custom
TagList: TagList:
type: array type: array
items: items:

View file

@ -9,6 +9,8 @@ from openapi_core.schema.schemas.exceptions import (
) )
from openapi_core.schema.schemas.models import Schema from openapi_core.schema.schemas.models import Schema
from six import b, u
class TestSchemaIteritems(object): class TestSchemaIteritems(object):
@ -77,13 +79,22 @@ class TestSchemaUnmarshal(object):
assert result == datetime.date(2018, 1, 2) assert result == datetime.date(2018, 1, 2)
def test_string_format_datetime(self):
schema = Schema('string', schema_format='date-time')
value = '2018-01-02T00:00:00'
result = schema.unmarshal(value)
assert result == datetime.datetime(2018, 1, 2, 0, 0, 0)
@pytest.mark.xfail(reason="No custom formats support atm")
def test_string_format_custom(self): def test_string_format_custom(self):
custom_format = 'custom' custom_format = 'custom'
schema = Schema('string', schema_format=custom_format) schema = Schema('string', schema_format=custom_format)
value = 'x' value = 'x'
with mock.patch.dict( with mock.patch.dict(
Schema.FORMAT_CALLABLE_GETTER, Schema.STRING_FORMAT_CAST_CALLABLE_GETTER,
{custom_format: lambda x: x + '-custom'}, {custom_format: lambda x: x + '-custom'},
): ):
result = schema.unmarshal(value) result = schema.unmarshal(value)
@ -95,17 +106,17 @@ class TestSchemaUnmarshal(object):
schema = Schema('string', schema_format=unknown_format) schema = Schema('string', schema_format=unknown_format)
value = 'x' value = 'x'
result = schema.unmarshal(value) with pytest.raises(OpenAPISchemaError):
schema.unmarshal(value)
assert result == 'x'
@pytest.mark.xfail(reason="No custom formats support atm")
def test_string_format_invalid_value(self): def test_string_format_invalid_value(self):
custom_format = 'custom' custom_format = 'custom'
schema = Schema('string', schema_format=custom_format) schema = Schema('string', schema_format=custom_format)
value = 'x' value = 'x'
with mock.patch.dict( with mock.patch.dict(
Schema.FORMAT_CALLABLE_GETTER, Schema.STRING_FORMAT_CAST_CALLABLE_GETTER,
{custom_format: mock.Mock(side_effect=ValueError())}, {custom_format: mock.Mock(side_effect=ValueError())},
), pytest.raises( ), pytest.raises(
InvalidSchemaValue, message='Failed to format value' InvalidSchemaValue, message='Failed to format value'
@ -191,7 +202,7 @@ class TestSchemaValidate(object):
assert result == value assert result == value
@pytest.mark.parametrize('value', [1, 3.14, 'true', [True, False]]) @pytest.mark.parametrize('value', [1, 3.14, u('true'), [True, False]])
def test_boolean_invalid(self, value): def test_boolean_invalid(self, value):
schema = Schema('boolean') schema = Schema('boolean')
@ -213,7 +224,7 @@ class TestSchemaValidate(object):
assert result == value assert result == value
@pytest.mark.parametrize('value', [False, 1, 3.14, 'true']) @pytest.mark.parametrize('value', [False, 1, 3.14, u('true')])
def test_array_invalid(self, value): def test_array_invalid(self, value):
schema = Schema('array') schema = Schema('array')
@ -228,7 +239,7 @@ class TestSchemaValidate(object):
assert result == value assert result == value
@pytest.mark.parametrize('value', [False, 3.14, 'true', [1, 2]]) @pytest.mark.parametrize('value', [False, 3.14, u('true'), [1, 2]])
def test_integer_invalid(self, value): def test_integer_invalid(self, value):
schema = Schema('integer') schema = Schema('integer')
@ -250,7 +261,7 @@ class TestSchemaValidate(object):
with pytest.raises(InvalidSchemaValue): with pytest.raises(InvalidSchemaValue):
schema.validate(value) schema.validate(value)
@pytest.mark.parametrize('value', ['true', b'true']) @pytest.mark.parametrize('value', [u('true'), ])
def test_string(self, value): def test_string(self, value):
schema = Schema('string') schema = Schema('string')
@ -258,13 +269,85 @@ class TestSchemaValidate(object):
assert result == value assert result == value
@pytest.mark.parametrize('value', [False, 1, 3.14, [1, 3]]) @pytest.mark.parametrize('value', [b('test'), False, 1, 3.14, [1, 3]])
def test_string_invalid(self, value): def test_string_invalid(self, value):
schema = Schema('string') schema = Schema('string')
with pytest.raises(InvalidSchemaValue): with pytest.raises(InvalidSchemaValue):
schema.validate(value) schema.validate(value)
@pytest.mark.parametrize('value', [
b('true'), u('test'), False, 1, 3.14, [1, 3],
datetime.datetime(1989, 1, 2),
])
def test_string_format_date_invalid(self, value):
schema = Schema('string', schema_format='date')
with pytest.raises(InvalidSchemaValue):
schema.validate(value)
@pytest.mark.parametrize('value', [
datetime.date(1989, 1, 2), datetime.date(2018, 1, 2),
])
def test_string_format_date(self, value):
schema = Schema('string', schema_format='date')
result = schema.validate(value)
assert result == value
@pytest.mark.parametrize('value', [
b('true'), u('true'), False, 1, 3.14, [1, 3],
datetime.date(1989, 1, 2),
])
def test_string_format_datetime_invalid(self, value):
schema = Schema('string', schema_format='date-time')
with pytest.raises(InvalidSchemaValue):
schema.validate(value)
@pytest.mark.parametrize('value', [
datetime.datetime(1989, 1, 2, 0, 0, 0),
datetime.datetime(2018, 1, 2, 23, 59, 59),
])
def test_string_format_datetime(self, value):
schema = Schema('string', schema_format='date-time')
result = schema.validate(value)
assert result == value
@pytest.mark.parametrize('value', [
u('true'), False, 1, 3.14, [1, 3], datetime.date(1989, 1, 2),
datetime.datetime(1989, 1, 2, 0, 0, 0),
])
def test_string_format_binary_invalid(self, value):
schema = Schema('string', schema_format='binary')
with pytest.raises(InvalidSchemaValue):
schema.validate(value)
@pytest.mark.parametrize('value', [
b('stream'), b('text'),
])
def test_string_format_binary(self, value):
schema = Schema('string', schema_format='binary')
result = schema.validate(value)
assert result == value
@pytest.mark.parametrize('value', [
u('test'), b('stream'), datetime.date(1989, 1, 2),
datetime.datetime(1989, 1, 2, 0, 0, 0),
])
def test_string_format_unknown(self, value):
unknown_format = 'unknown'
schema = Schema('string', schema_format=unknown_format)
with pytest.raises(OpenAPISchemaError):
schema.validate(value)
@pytest.mark.parametrize('value', ['true', False, 1, 3.14, [1, 3]]) @pytest.mark.parametrize('value', ['true', False, 1, 3.14, [1, 3]])
def test_object_not_an_object(self, value): def test_object_not_an_object(self, value):
schema = Schema('object') schema = Schema('object')