From 9d9629b4bdef6e24502747048cb328163a54bfe7 Mon Sep 17 00:00:00 2001 From: Artur Maciag Date: Thu, 23 May 2019 12:48:45 +0100 Subject: [PATCH] Primitive types unmarshallers --- openapi_core/schema/media_types/models.py | 3 +- openapi_core/schema/parameters/models.py | 3 +- openapi_core/schema/schemas/exceptions.py | 14 +++ openapi_core/schema/schemas/models.py | 100 ++++++----------- openapi_core/schema/schemas/unmarshallers.py | 107 +++++++++++++++++++ 5 files changed, 160 insertions(+), 67 deletions(-) create mode 100644 openapi_core/schema/schemas/unmarshallers.py diff --git a/openapi_core/schema/media_types/models.py b/openapi_core/schema/media_types/models.py index 5104095..7c53945 100644 --- a/openapi_core/schema/media_types/models.py +++ b/openapi_core/schema/media_types/models.py @@ -47,6 +47,7 @@ class MediaType(object): raise InvalidMediaTypeValue(exc) try: - return self.schema.validate(unmarshalled, custom_formatters=custom_formatters) + return self.schema.validate( + unmarshalled, custom_formatters=custom_formatters) except OpenAPISchemaError as exc: raise InvalidMediaTypeValue(exc) diff --git a/openapi_core/schema/parameters/models.py b/openapi_core/schema/parameters/models.py index 090bf0a..544f750 100644 --- a/openapi_core/schema/parameters/models.py +++ b/openapi_core/schema/parameters/models.py @@ -118,6 +118,7 @@ class Parameter(object): raise InvalidParameterValue(self.name, exc) try: - return self.schema.validate(unmarshalled, custom_formatters=custom_formatters) + return self.schema.validate( + unmarshalled, custom_formatters=custom_formatters) except OpenAPISchemaError as exc: raise InvalidParameterValue(self.name, exc) diff --git a/openapi_core/schema/schemas/exceptions.py b/openapi_core/schema/schemas/exceptions.py index b4656aa..a370aac 100644 --- a/openapi_core/schema/schemas/exceptions.py +++ b/openapi_core/schema/schemas/exceptions.py @@ -77,3 +77,17 @@ class MultipleOneOfSchema(OpenAPISchemaError): def __str__(self): return "Exactly one schema type {0} should be valid, more than one found".format(self.type) + + +class UnmarshallerError(Exception): + pass + + +@attr.s +class UnmarshallerStrictTypeError(UnmarshallerError): + value = attr.ib() + types = attr.ib() + + def __str__(self): + return "Value {value} is not one of types {types}".format( + self.value, self.types) diff --git a/openapi_core/schema/schemas/models.py b/openapi_core/schema/schemas/models.py index 9728ea9..ffe52ab 100644 --- a/openapi_core/schema/schemas/models.py +++ b/openapi_core/schema/schemas/models.py @@ -16,6 +16,7 @@ from openapi_core.schema.schemas.exceptions import ( InvalidSchemaValue, UndefinedSchemaProperty, MissingSchemaProperty, OpenAPISchemaError, NoOneOfSchema, MultipleOneOfSchema, NoValidSchema, UndefinedItemsSchema, InvalidCustomFormatSchemaValue, InvalidSchemaProperty, + UnmarshallerStrictTypeError, ) from openapi_core.schema.schemas.util import ( forcebool, format_date, format_datetime, format_byte, format_uuid, @@ -155,14 +156,19 @@ class Schema(object): return set(required) def get_cast_mapping(self, custom_formatters=None, strict=True): + primitive_unmarshallers = self.get_primitive_unmarshallers( + custom_formatters=custom_formatters) + + primitive_unmarshallers_partial = dict( + (t, functools.partial(u, type_format=self.format, strict=strict)) + for t, u in primitive_unmarshallers.items() + ) + pass_defaults = lambda f: functools.partial( f, custom_formatters=custom_formatters, strict=strict) mapping = self.DEFAULT_CAST_CALLABLE_GETTER.copy() + mapping.update(primitive_unmarshallers_partial) mapping.update({ - SchemaType.STRING: pass_defaults(self._unmarshal_string), - SchemaType.BOOLEAN: pass_defaults(self._unmarshal_boolean), - SchemaType.INTEGER: pass_defaults(self._unmarshal_integer), - SchemaType.NUMBER: pass_defaults(self._unmarshal_number), SchemaType.ANY: pass_defaults(self._unmarshal_any), SchemaType.ARRAY: pass_defaults(self._unmarshal_collection), SchemaType.OBJECT: pass_defaults(self._unmarshal_object), @@ -184,6 +190,10 @@ class Schema(object): raise InvalidSchemaValue("Null value for non-nullable schema", value, self.type) return self.default + if self.enum and value not in self.enum: + raise InvalidSchemaValue( + "Value {value} not in enum choices: {type}", value, self.enum) + cast_mapping = self.get_cast_mapping( custom_formatters=custom_formatters, strict=strict) @@ -193,6 +203,9 @@ class Schema(object): cast_callable = cast_mapping[self.type] try: return cast_callable(value) + except UnmarshallerStrictTypeError: + raise InvalidSchemaValue( + "Value {value} is not of type {type}", value, self.type) except ValueError: raise InvalidSchemaValue( "Failed to cast value {value} to type {type}", value, self.type) @@ -207,72 +220,27 @@ class Schema(object): if casted is None and not self.required: return None - if self.enum and casted not in self.enum: - raise InvalidSchemaValue( - "Value {value} not in enum choices: {type}", value, self.enum) - return casted - def _unmarshal_string(self, value, custom_formatters=None, strict=True): - if strict and not isinstance(value, (text_type, binary_type)): - raise InvalidSchemaValue("Value {value} is not of type {type}", value, self.type) + def get_primitive_unmarshallers(self, **options): + from openapi_core.schema.schemas.unmarshallers import ( + StringUnmarshaller, BooleanUnmarshaller, IntegerUnmarshaller, + NumberUnmarshaller, + ) - try: - schema_format = SchemaFormat(self.format) - except ValueError: - msg = "Unsupported format {type} unmarshalling for value {value}" - if custom_formatters is not None: - formatstring = custom_formatters.get(self.format) - if formatstring is None: - raise InvalidSchemaValue(msg, value, self.format) - else: - raise InvalidSchemaValue(msg, value, self.format) - else: - if self.enum and value not in self.enum: - raise InvalidSchemaValue( - "Value {value} not in enum choices: {type}", value, self.enum) - formatstring = self.STRING_FORMAT_CALLABLE_GETTER[schema_format] + unmarshallers_classes = { + SchemaType.STRING: StringUnmarshaller, + SchemaType.BOOLEAN: BooleanUnmarshaller, + SchemaType.INTEGER: IntegerUnmarshaller, + SchemaType.NUMBER: NumberUnmarshaller, + } - try: - return formatstring.unmarshal(value) - except ValueError as exc: - raise InvalidCustomFormatSchemaValue( - "Failed to format value {value} to format {type}: {exception}", value, self.format, exc) + unmarshallers = dict( + (t, klass(**options)) + for t, klass in unmarshallers_classes.items() + ) - def _unmarshal_integer(self, value, custom_formatters=None, strict=True): - if strict and not isinstance(value, integer_types): - raise InvalidSchemaValue("Value {value} is not of type {type}", value, self.type) - - return int(value) - - def _unmarshal_number(self, value, custom_formatters=None, strict=True): - if strict and not isinstance(value, (float, ) + integer_types): - raise InvalidSchemaValue("Value {value} is not of type {type}", value, self.type) - - try: - schema_format = SchemaFormat(self.format) - except ValueError: - msg = "Unsupported format {type} unmarshalling for value {value}" - if custom_formatters is not None: - formatnumber = custom_formatters.get(self.format) - if formatnumber is None: - raise InvalidSchemaValue(msg, value, self.format) - else: - raise InvalidSchemaValue(msg, value, self.format) - else: - formatnumber = self.NUMBER_FORMAT_CALLABLE_GETTER[schema_format] - - try: - return formatnumber.unmarshal(value) - except ValueError as exc: - raise InvalidCustomFormatSchemaValue( - "Failed to format value {value} to format {type}: {exception}", value, self.format, exc) - - def _unmarshal_boolean(self, value, custom_formatters=None, strict=True): - if strict and not isinstance(value, (bool, )): - raise InvalidSchemaValue("Value {value} is not of type {type}", value, self.type) - - return forcebool(value) + return unmarshallers def _unmarshal_any(self, value, custom_formatters=None, strict=True): types_resolve_order = [ @@ -301,6 +269,8 @@ class Schema(object): cast_callable = cast_mapping[schema_type] try: return cast_callable(value) + except UnmarshallerStrictTypeError: + continue # @todo: remove ValueError when validation separated except (OpenAPISchemaError, TypeError, ValueError): continue diff --git a/openapi_core/schema/schemas/unmarshallers.py b/openapi_core/schema/schemas/unmarshallers.py new file mode 100644 index 0000000..86fd46b --- /dev/null +++ b/openapi_core/schema/schemas/unmarshallers.py @@ -0,0 +1,107 @@ +from six import text_type, binary_type, integer_types + +from openapi_core.schema.schemas.enums import SchemaFormat, SchemaType +from openapi_core.schema.schemas.exceptions import ( + InvalidSchemaValue, InvalidCustomFormatSchemaValue, + OpenAPISchemaError, MultipleOneOfSchema, NoOneOfSchema, + InvalidSchemaProperty, + UnmarshallerStrictTypeError, +) +from openapi_core.schema.schemas.util import ( + forcebool, format_date, format_datetime, format_byte, format_uuid, + format_number, +) + + +class StrictUnmarshaller(object): + + STRICT_TYPES = () + + def __call__(self, value, type_format=SchemaFormat.NONE, strict=True): + if self.STRICT_TYPES and strict and not isinstance( + value, self.STRICT_TYPES): + raise UnmarshallerStrictTypeError(value, self.STRICT_TYPES) + + return value + + +class PrimitiveTypeUnmarshaller(StrictUnmarshaller): + + FORMATTERS = { + SchemaFormat.NONE: lambda x: x, + } + + def __init__(self, custom_formatters=None): + if custom_formatters is None: + custom_formatters = {} + self.custom_formatters = custom_formatters + + def __call__(self, value, type_format=SchemaFormat.NONE, strict=True): + value = super(PrimitiveTypeUnmarshaller, self).__call__( + value, type_format=type_format, strict=strict) + + try: + schema_format = SchemaFormat(type_format) + except ValueError: + formatter = self.custom_formatters.get(type_format) + else: + formatters = self.get_formatters() + formatter = formatters.get(schema_format) + + if formatter is None: + raise InvalidSchemaValue( + "Unsupported format {type} unmarshalling " + "for value {value}", + value, type_format) + + try: + return formatter(value) + except ValueError as exc: + raise InvalidCustomFormatSchemaValue( + "Failed to format value {value} to format {type}: {exception}", + value, type_format, exc) + + def get_formatters(self): + return self.FORMATTERS + + +class StringUnmarshaller(PrimitiveTypeUnmarshaller): + + STRICT_TYPES = (text_type, binary_type) + FORMATTERS = { + SchemaFormat.NONE: text_type, + SchemaFormat.PASSWORD: text_type, + SchemaFormat.DATE: format_date, + SchemaFormat.DATETIME: format_datetime, + SchemaFormat.BINARY: binary_type, + SchemaFormat.UUID: format_uuid, + SchemaFormat.BYTE: format_byte, + } + + +class IntegerUnmarshaller(PrimitiveTypeUnmarshaller): + + STRICT_TYPES = integer_types + FORMATTERS = { + SchemaFormat.NONE: int, + SchemaFormat.INT32: int, + SchemaFormat.INT64: int, + } + + +class NumberUnmarshaller(PrimitiveTypeUnmarshaller): + + STRICT_TYPES = (float, ) + integer_types + FORMATTERS = { + SchemaFormat.NONE: format_number, + SchemaFormat.FLOAT: float, + SchemaFormat.DOUBLE: float, + } + + +class BooleanUnmarshaller(PrimitiveTypeUnmarshaller): + + STRICT_TYPES = (bool, ) + FORMATTERS = { + SchemaFormat.NONE: forcebool, + }