diff --git a/openapi_core/schema/schemas/models.py b/openapi_core/schema/schemas/models.py index 22a79eb..f62b3f9 100644 --- a/openapi_core/schema/schemas/models.py +++ b/openapi_core/schema/schemas/models.py @@ -1,6 +1,7 @@ """OpenAPI core schemas models module""" import logging from collections import defaultdict +from datetime import date, datetime import warnings from six import iteritems, integer_types, binary_type, text_type @@ -11,7 +12,9 @@ from openapi_core.schema.schemas.exceptions import ( InvalidSchemaValue, UndefinedSchemaProperty, MissingSchemaProperty, 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 ( TypeValidator, AttributeValidator, ) @@ -28,16 +31,27 @@ class Schema(object): SchemaType.BOOLEAN: forcebool, } - FORMAT_CALLABLE_GETTER = defaultdict(lambda: lambda x: x, { - SchemaFormat.DATE.value: format_date, - }) + STRING_FORMAT_CAST_CALLABLE_GETTER = { + 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 = { - None: lambda x: x, + None: lambda x: True, 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.STRING: TypeValidator( + text_type, date, datetime, binary_type), SchemaType.ARRAY: TypeValidator(list, tuple), SchemaType.OBJECT: AttributeValidator('__dict__'), } @@ -158,7 +172,16 @@ class Schema(object): return casted 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: return formatter(value) except ValueError: @@ -244,6 +267,7 @@ class Schema(object): def get_validator_mapping(self): mapping = { SchemaType.ARRAY: self._validate_collection, + SchemaType.STRING: self._validate_string, SchemaType.OBJECT: self._validate_object, } @@ -277,6 +301,26 @@ class Schema(object): 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): properties = value.__dict__ diff --git a/openapi_core/schema/schemas/util.py b/openapi_core/schema/schemas/util.py index 3291276..ce3b977 100644 --- a/openapi_core/schema/schemas/util.py +++ b/openapi_core/schema/schemas/util.py @@ -18,3 +18,7 @@ def dicthash(d): def format_date(value): return datetime.datetime.strptime(value, '%Y-%m-%d').date() + + +def format_datetime(value): + return datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%S') diff --git a/tests/integration/data/v3.0/petstore.yaml b/tests/integration/data/v3.0/petstore.yaml index 4a91d23..c01ff6b 100644 --- a/tests/integration/data/v3.0/petstore.yaml +++ b/tests/integration/data/v3.0/petstore.yaml @@ -286,7 +286,6 @@ components: properties: name: type: string - format: custom TagList: type: array items: diff --git a/tests/unit/schema/test_schemas.py b/tests/unit/schema/test_schemas.py index dde4d06..2709e63 100644 --- a/tests/unit/schema/test_schemas.py +++ b/tests/unit/schema/test_schemas.py @@ -9,6 +9,8 @@ from openapi_core.schema.schemas.exceptions import ( ) from openapi_core.schema.schemas.models import Schema +from six import b, u + class TestSchemaIteritems(object): @@ -77,13 +79,22 @@ class TestSchemaUnmarshal(object): 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): custom_format = 'custom' schema = Schema('string', schema_format=custom_format) value = 'x' with mock.patch.dict( - Schema.FORMAT_CALLABLE_GETTER, + Schema.STRING_FORMAT_CAST_CALLABLE_GETTER, {custom_format: lambda x: x + '-custom'}, ): result = schema.unmarshal(value) @@ -95,17 +106,17 @@ class TestSchemaUnmarshal(object): schema = Schema('string', schema_format=unknown_format) value = 'x' - result = schema.unmarshal(value) - - assert result == 'x' + with pytest.raises(OpenAPISchemaError): + schema.unmarshal(value) + @pytest.mark.xfail(reason="No custom formats support atm") def test_string_format_invalid_value(self): custom_format = 'custom' schema = Schema('string', schema_format=custom_format) value = 'x' with mock.patch.dict( - Schema.FORMAT_CALLABLE_GETTER, + Schema.STRING_FORMAT_CAST_CALLABLE_GETTER, {custom_format: mock.Mock(side_effect=ValueError())}, ), pytest.raises( InvalidSchemaValue, message='Failed to format value' @@ -191,7 +202,7 @@ class TestSchemaValidate(object): 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): schema = Schema('boolean') @@ -213,7 +224,7 @@ class TestSchemaValidate(object): 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): schema = Schema('array') @@ -228,7 +239,7 @@ class TestSchemaValidate(object): 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): schema = Schema('integer') @@ -250,7 +261,7 @@ class TestSchemaValidate(object): with pytest.raises(InvalidSchemaValue): schema.validate(value) - @pytest.mark.parametrize('value', ['true', b'true']) + @pytest.mark.parametrize('value', [u('true'), ]) def test_string(self, value): schema = Schema('string') @@ -258,13 +269,85 @@ class TestSchemaValidate(object): 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): schema = Schema('string') with pytest.raises(InvalidSchemaValue): 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]]) def test_object_not_an_object(self, value): schema = Schema('object')