diff --git a/openapi_core/schema/schemas/models.py b/openapi_core/schema/schemas/models.py index b783cae..44fa022 100644 --- a/openapi_core/schema/schemas/models.py +++ b/openapi_core/schema/schemas/models.py @@ -6,12 +6,12 @@ import warnings from six import iteritems from openapi_core.extensions.models.factories import ModelFactory -from openapi_core.schema.schemas.enums import SchemaType +from openapi_core.schema.schemas.enums import SchemaFormat, SchemaType from openapi_core.schema.schemas.exceptions import ( InvalidSchemaValue, UndefinedSchemaProperty, MissingSchemaProperty, OpenAPISchemaError, NoOneOfSchema, MultipleOneOfSchema, ) -from openapi_core.schema.schemas.util import forcebool +from openapi_core.schema.schemas.util import forcebool, format_date log = logging.getLogger(__name__) @@ -25,6 +25,10 @@ class Schema(object): SchemaType.BOOLEAN: forcebool, } + FORMAT_CALLABLE_GETTER = defaultdict(lambda: lambda x: x, { + SchemaFormat.DATE.value: format_date, + }) + def __init__( self, schema_type=None, model=None, properties=None, items=None, schema_format=None, required=None, default=None, nullable=False, @@ -92,6 +96,7 @@ class Schema(object): def get_cast_mapping(self): mapping = self.DEFAULT_CAST_CALLABLE_GETTER.copy() mapping.update({ + SchemaType.STRING: self._unmarshal_string, SchemaType.ARRAY: self._unmarshal_collection, SchemaType.OBJECT: self._unmarshal_object, }) @@ -110,7 +115,7 @@ class Schema(object): cast_mapping = self.get_cast_mapping() - if self.type in cast_mapping and value == '': + if self.type is not SchemaType.STRING and value == '': return None cast_callable = cast_mapping[self.type] @@ -139,6 +144,16 @@ class Schema(object): return casted + def _unmarshal_string(self, value): + formatter = self.FORMAT_CALLABLE_GETTER[self.format] + try: + return formatter(value) + except ValueError: + raise InvalidSchemaValue( + "Failed to format value of {0} to {1}".format( + value, self.format) + ) + def _unmarshal_collection(self, value): return list(map(self.items.unmarshal, value)) diff --git a/openapi_core/schema/schemas/util.py b/openapi_core/schema/schemas/util.py index 337b925..3291276 100644 --- a/openapi_core/schema/schemas/util.py +++ b/openapi_core/schema/schemas/util.py @@ -1,4 +1,5 @@ """OpenAPI core schemas util module""" +import datetime from distutils.util import strtobool from json import dumps from six import string_types @@ -13,3 +14,7 @@ def forcebool(val): def dicthash(d): return hash(dumps(d, sort_keys=True)) + + +def format_date(value): + return datetime.datetime.strptime(value, '%Y-%m-%d').date() diff --git a/tests/unit/schema/test_schemas.py b/tests/unit/schema/test_schemas.py index 423b098..ed4f8fe 100644 --- a/tests/unit/schema/test_schemas.py +++ b/tests/unit/schema/test_schemas.py @@ -1,3 +1,5 @@ +import datetime + import mock import pytest @@ -64,6 +66,49 @@ class TestSchemaUnmarshal(object): assert result == default_value + def test_string_format_date(self): + schema = Schema('string', schema_format='date') + value = '2018-01-02' + + result = schema.unmarshal(value) + + assert result == datetime.date(2018, 1, 2) + + 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, + {custom_format: lambda x: x + '-custom'}, + ): + result = schema.unmarshal(value) + + assert result == 'x-custom' + + def test_string_format_unknown(self): + unknown_format = 'unknown' + schema = Schema('string', schema_format=unknown_format) + value = 'x' + + result = schema.unmarshal(value) + + assert result == 'x' + + 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, + {custom_format: mock.Mock(side_effect=ValueError())}, + ), pytest.raises( + InvalidSchemaValue, message='Failed to format value' + ): + schema.unmarshal(value) + def test_integer_valid(self): schema = Schema('integer') value = '123'