diff --git a/openapi_core/exceptions.py b/openapi_core/exceptions.py new file mode 100644 index 0000000..504173c --- /dev/null +++ b/openapi_core/exceptions.py @@ -0,0 +1,5 @@ +"""OpenAPI core exceptions module""" + + +class OpenAPIError(Exception): + pass diff --git a/openapi_core/schema/exceptions.py b/openapi_core/schema/exceptions.py index 029e48b..3c1e93d 100644 --- a/openapi_core/schema/exceptions.py +++ b/openapi_core/schema/exceptions.py @@ -1,8 +1,5 @@ """OpenAPI core schema exceptions module""" - - -class OpenAPIError(Exception): - pass +from openapi_core.exceptions import OpenAPIError class OpenAPIMappingError(OpenAPIError): diff --git a/openapi_core/schema/extensions/__init__.py b/openapi_core/schema/extensions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openapi_core/schema/extensions/generators.py b/openapi_core/schema/extensions/generators.py new file mode 100644 index 0000000..1cfc459 --- /dev/null +++ b/openapi_core/schema/extensions/generators.py @@ -0,0 +1,16 @@ +"""OpenAPI core extensions generators module""" +from six import iteritems + +from openapi_core.schema.extensions.models import Extension + + +class ExtensionsGenerator(object): + + def __init__(self, dereferencer): + self.dereferencer = dereferencer + + def generate(self, item_spec): + for field_name, value in iteritems(item_spec): + if not field_name.startswith('x-'): + continue + yield field_name, Extension(field_name, value) diff --git a/openapi_core/schema/extensions/models.py b/openapi_core/schema/extensions/models.py new file mode 100644 index 0000000..5575cfc --- /dev/null +++ b/openapi_core/schema/extensions/models.py @@ -0,0 +1,9 @@ +"""OpenAPI core extensions models module""" + + +class Extension(object): + """Represents an OpenAPI Extension.""" + + def __init__(self, field_name, value=None): + self.field_name = field_name + self.value = value diff --git a/openapi_core/schema/media_types/models.py b/openapi_core/schema/media_types/models.py index b0ebfb4..ba31ef1 100644 --- a/openapi_core/schema/media_types/models.py +++ b/openapi_core/schema/media_types/models.py @@ -5,8 +5,9 @@ from json import loads from openapi_core.schema.media_types.exceptions import InvalidMediaTypeValue from openapi_core.schema.schemas.exceptions import ( - CastError, ValidateError, UnmarshalError, + CastError, ValidateError, ) +from openapi_core.unmarshalling.schemas.exceptions import UnmarshalError MEDIA_TYPE_DESERIALIZERS = { diff --git a/openapi_core/schema/parameters/models.py b/openapi_core/schema/parameters/models.py index 766116d..61e1737 100644 --- a/openapi_core/schema/parameters/models.py +++ b/openapi_core/schema/parameters/models.py @@ -11,8 +11,9 @@ from openapi_core.schema.parameters.exceptions import ( ) from openapi_core.schema.schemas.enums import SchemaType from openapi_core.schema.schemas.exceptions import ( - CastError, ValidateError, UnmarshalError, + CastError, ValidateError, ) +from openapi_core.unmarshalling.schemas.exceptions import UnmarshalError log = logging.getLogger(__name__) @@ -81,10 +82,7 @@ class Parameter(object): if self.required: raise MissingRequiredParameter(self.name) - if not self.schema or self.schema.default is None: - raise MissingParameter(self.name) - - return self.schema.default + raise MissingParameter(self.name) if self.aslist and self.explode: if hasattr(location, 'getall'): diff --git a/openapi_core/schema/schemas/exceptions.py b/openapi_core/schema/schemas/exceptions.py index 568812a..308fdeb 100644 --- a/openapi_core/schema/schemas/exceptions.py +++ b/openapi_core/schema/schemas/exceptions.py @@ -23,27 +23,6 @@ class ValidateError(OpenAPISchemaError): pass -class UnmarshalError(OpenAPISchemaError): - """Schema unmarshal operation error""" - pass - - -@attr.s(hash=True) -class UnmarshalValueError(UnmarshalError): - """Failed to unmarshal value to type""" - value = attr.ib() - type = attr.ib() - original_exception = attr.ib(default=None) - - def __str__(self): - return ( - "Failed to unmarshal value {value} to type {type}: {exception}" - ).format( - value=self.value, type=self.type, - exception=self.original_exception, - ) - - @attr.s(hash=True) class InvalidSchemaValue(ValidateError): value = attr.ib() @@ -61,48 +40,3 @@ class InvalidSchemaValue(ValidateError): return ( "Value {value} not valid for schema of type {type}: {errors}" ).format(value=self.value, type=self.type, errors=self.schema_errors) - - -class UnmarshallerError(UnmarshalError): - """Unmarshaller error""" - pass - - -@attr.s(hash=True) -class InvalidCustomFormatSchemaValue(UnmarshallerError): - """Value failed to format with custom formatter""" - value = attr.ib() - type = attr.ib() - original_exception = attr.ib() - - def __str__(self): - return ( - "Failed to format value {value} to format {type}: {exception}" - ).format( - value=self.value, type=self.type, - exception=self.original_exception, - ) - - -@attr.s(hash=True) -class FormatterNotFoundError(UnmarshallerError): - """Formatter not found to unmarshal""" - value = attr.ib() - type_format = attr.ib() - - def __str__(self): - return ( - "Formatter not found for {format} format " - "to unmarshal value {value}" - ).format(format=self.type_format, value=self.value) - - -@attr.s(hash=True) -class UnmarshallerStrictTypeError(UnmarshallerError): - value = attr.ib() - types = attr.ib() - - def __str__(self): - types = ', '.join(list(map(str, self.types))) - return "Value {value} is not one of types: {types}".format( - value=self.value, types=types) diff --git a/openapi_core/schema/schemas/factories.py b/openapi_core/schema/schemas/factories.py index 00e3f02..f92d598 100644 --- a/openapi_core/schema/schemas/factories.py +++ b/openapi_core/schema/schemas/factories.py @@ -4,9 +4,10 @@ import logging from six import iteritems from openapi_core.compat import lru_cache +from openapi_core.schema.extensions.generators import ExtensionsGenerator from openapi_core.schema.properties.generators import PropertiesGenerator from openapi_core.schema.schemas.models import Schema -from openapi_core.schema.schemas.types import Contribution +from openapi_core.schema.schemas.types import Contribution, NoValue log = logging.getLogger(__name__) @@ -21,9 +22,8 @@ class SchemaFactory(object): schema_type = schema_deref.get('type', None) schema_format = schema_deref.get('format') - model = schema_deref.get('x-model', None) required = schema_deref.get('required', False) - default = schema_deref.get('default', None) + default = schema_deref.get('default', NoValue) properties_spec = schema_deref.get('properties', None) items_spec = schema_deref.get('items', None) nullable = schema_deref.get('nullable', False) @@ -47,6 +47,8 @@ class SchemaFactory(object): min_properties = schema_deref.get('minProperties', None) max_properties = schema_deref.get('maxProperties', None) + extensions = self.extensions_generator.generate(schema_deref) + properties = None if properties_spec: properties = self.properties_generator.generate(properties_spec) @@ -68,7 +70,7 @@ class SchemaFactory(object): additional_properties = self.create(additional_properties_spec) return Schema( - schema_type=schema_type, model=model, properties=properties, + schema_type=schema_type, properties=properties, items=items, schema_format=schema_format, required=required, default=default, nullable=nullable, enum=enum, deprecated=deprecated, all_of=all_of, one_of=one_of, @@ -79,9 +81,15 @@ class SchemaFactory(object): exclusive_maximum=exclusive_maximum, exclusive_minimum=exclusive_minimum, min_properties=min_properties, max_properties=max_properties, + extensions=extensions, _source=schema_deref, ) + @property + @lru_cache() + def extensions_generator(self): + return ExtensionsGenerator(self.dereferencer) + @property @lru_cache() def properties_generator(self): diff --git a/openapi_core/schema/schemas/models.py b/openapi_core/schema/schemas/models.py index 5095748..f18c4ee 100644 --- a/openapi_core/schema/schemas/models.py +++ b/openapi_core/schema/schemas/models.py @@ -1,23 +1,22 @@ """OpenAPI core schemas models module""" import attr -import functools import logging from collections import defaultdict import re -import warnings -from six import iteritems from jsonschema.exceptions import ValidationError -from openapi_core.extensions.models.factories import ModelFactory from openapi_core.schema.schemas._format import oas30_format_checker from openapi_core.schema.schemas.enums import SchemaType from openapi_core.schema.schemas.exceptions import ( CastError, InvalidSchemaValue, - UnmarshalValueError, UnmarshalError, ) +from openapi_core.schema.schemas.types import NoValue from openapi_core.schema.schemas.util import forcebool from openapi_core.schema.schemas.validators import OAS30Validator +from openapi_core.unmarshalling.schemas.exceptions import ( + UnmarshalValueError, +) log = logging.getLogger(__name__) @@ -37,20 +36,17 @@ class Schema(object): SchemaType.BOOLEAN: forcebool, } - DEFAULT_UNMARSHAL_CALLABLE_GETTER = { - } - def __init__( - self, schema_type=None, model=None, properties=None, items=None, - schema_format=None, required=None, default=None, nullable=False, + self, schema_type=None, properties=None, items=None, + schema_format=None, required=None, default=NoValue, nullable=False, enum=None, deprecated=False, all_of=None, one_of=None, additional_properties=True, min_items=None, max_items=None, min_length=None, max_length=None, pattern=None, unique_items=False, minimum=None, maximum=None, multiple_of=None, exclusive_minimum=False, exclusive_maximum=False, - min_properties=None, max_properties=None, _source=None): + min_properties=None, max_properties=None, extensions=None, + _source=None): self.type = SchemaType(schema_type) - self.model = model self.properties = properties and dict(properties) or {} self.items = items self.format = schema_format @@ -79,6 +75,8 @@ class Schema(object): self.max_properties = int(max_properties)\ if max_properties is not None else None + self.extensions = extensions and dict(extensions) or {} + self._all_required_properties_cache = None self._all_optional_properties_cache = None @@ -95,6 +93,9 @@ class Schema(object): def __getitem__(self, name): return self.properties[name] + def has_default(self): + return self.default is not NoValue + def get_all_properties(self): properties = self.properties.copy() @@ -108,32 +109,6 @@ class Schema(object): all_properties = self.get_all_properties() return set(all_properties.keys()) - def get_all_required_properties(self): - if self._all_required_properties_cache is None: - self._all_required_properties_cache =\ - self._get_all_required_properties() - - return self._all_required_properties_cache - - def _get_all_required_properties(self): - all_properties = self.get_all_properties() - required = self.get_all_required_properties_names() - - return dict( - (prop_name, val) - for prop_name, val in iteritems(all_properties) - if prop_name in required - ) - - def get_all_required_properties_names(self): - required = self.required[:] - - for subschema in self.all_of: - subschema_req = subschema.get_all_required_properties() - required += subschema_req - - return set(required) - def get_cast_mapping(self): mapping = self.TYPE_CAST_CALLABLE_GETTER.copy() mapping.update({ @@ -144,7 +119,7 @@ class Schema(object): def cast(self, value): """Cast value from string to schema type""" - if value is None: + if value in (None, NoValue): return value cast_mapping = self.get_cast_mapping() @@ -158,28 +133,6 @@ class Schema(object): def _cast_collection(self, value): return list(map(self.items.cast, value)) - def get_unmarshal_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() - ) - - def pass_defaults(f): - return functools.partial( - f, custom_formatters=custom_formatters, strict=strict) - mapping = self.DEFAULT_UNMARSHAL_CALLABLE_GETTER.copy() - mapping.update(primitive_unmarshallers_partial) - mapping.update({ - SchemaType.ANY: pass_defaults(self._unmarshal_any), - SchemaType.ARRAY: pass_defaults(self._unmarshal_collection), - SchemaType.OBJECT: pass_defaults(self._unmarshal_object), - }) - - return defaultdict(lambda: lambda x: x, mapping) - def get_validator(self, resolver=None): return OAS30Validator( self.__dict__, @@ -197,162 +150,13 @@ class Schema(object): def unmarshal(self, value, custom_formatters=None, strict=True): """Unmarshal parameter from the value.""" - if self.deprecated: - warnings.warn("The schema is deprecated", DeprecationWarning) - if value is None: - if not self.nullable: - raise UnmarshalError( - "Null value for non-nullable schema", value, self.type) - return self.default - - if self.enum and value not in self.enum: - raise UnmarshalError("Invalid value for enum: {0}".format(value)) - - unmarshal_mapping = self.get_unmarshal_mapping( - custom_formatters=custom_formatters, strict=strict) - - if self.type is not SchemaType.STRING and value == '': - return None - - unmarshal_callable = unmarshal_mapping[self.type] + from openapi_core.unmarshalling.schemas.factories import ( + SchemaUnmarshallersFactory, + ) + unmarshallers_factory = SchemaUnmarshallersFactory( + custom_formatters) + unmarshaller = unmarshallers_factory.create(self) try: - unmarshalled = unmarshal_callable(value) + return unmarshaller(value, strict=strict) except ValueError as exc: raise UnmarshalValueError(value, self.type, exc) - - return unmarshalled - - def get_primitive_unmarshallers(self, **options): - from openapi_core.schema.schemas.unmarshallers import ( - StringUnmarshaller, BooleanUnmarshaller, IntegerUnmarshaller, - NumberUnmarshaller, - ) - - unmarshallers_classes = { - SchemaType.STRING: StringUnmarshaller, - SchemaType.BOOLEAN: BooleanUnmarshaller, - SchemaType.INTEGER: IntegerUnmarshaller, - SchemaType.NUMBER: NumberUnmarshaller, - } - - unmarshallers = dict( - (t, klass(**options)) - for t, klass in unmarshallers_classes.items() - ) - - return unmarshallers - - def _unmarshal_any(self, value, custom_formatters=None, strict=True): - types_resolve_order = [ - SchemaType.OBJECT, SchemaType.ARRAY, SchemaType.BOOLEAN, - SchemaType.INTEGER, SchemaType.NUMBER, SchemaType.STRING, - ] - unmarshal_mapping = self.get_unmarshal_mapping() - if self.one_of: - result = None - for subschema in self.one_of: - try: - unmarshalled = subschema.unmarshal( - value, custom_formatters) - except UnmarshalError: - continue - else: - if result is not None: - log.warning("multiple valid oneOf schemas found") - continue - result = unmarshalled - - if result is None: - log.warning("valid oneOf schema not found") - - return result - else: - for schema_type in types_resolve_order: - unmarshal_callable = unmarshal_mapping[schema_type] - try: - return unmarshal_callable(value) - except (UnmarshalError, ValueError): - continue - - log.warning("failed to unmarshal any type") - return value - - def _unmarshal_collection( - self, value, custom_formatters=None, strict=True): - if not isinstance(value, (list, tuple)): - raise ValueError( - "Invalid value for collection: {0}".format(value)) - - f = functools.partial( - self.items.unmarshal, - custom_formatters=custom_formatters, strict=strict, - ) - return list(map(f, value)) - - def _unmarshal_object(self, value, model_factory=None, - custom_formatters=None, strict=True): - if not isinstance(value, (dict, )): - raise ValueError("Invalid value for object: {0}".format(value)) - - model_factory = model_factory or ModelFactory() - - if self.one_of: - properties = None - for one_of_schema in self.one_of: - try: - unmarshalled = self._unmarshal_properties( - value, one_of_schema, - custom_formatters=custom_formatters, - ) - except (UnmarshalError, ValueError): - pass - else: - if properties is not None: - log.warning("multiple valid oneOf schemas found") - continue - properties = unmarshalled - - if properties is None: - log.warning("valid oneOf schema not found") - - else: - properties = self._unmarshal_properties( - value, custom_formatters=custom_formatters) - - return model_factory.create(properties, name=self.model) - - def _unmarshal_properties(self, value, one_of_schema=None, - custom_formatters=None, strict=True): - all_props = self.get_all_properties() - all_props_names = self.get_all_properties_names() - all_req_props_names = self.get_all_required_properties_names() - - if one_of_schema is not None: - all_props.update(one_of_schema.get_all_properties()) - all_props_names |= one_of_schema.\ - get_all_properties_names() - all_req_props_names |= one_of_schema.\ - get_all_required_properties_names() - - value_props_names = value.keys() - extra_props = set(value_props_names) - set(all_props_names) - - properties = {} - if self.additional_properties is not True: - for prop_name in extra_props: - prop_value = value[prop_name] - properties[prop_name] = self.additional_properties.unmarshal( - prop_value, custom_formatters=custom_formatters) - - for prop_name, prop in iteritems(all_props): - try: - prop_value = value[prop_name] - except KeyError: - if not prop.nullable and not prop.default: - continue - prop_value = prop.default - - properties[prop_name] = prop.unmarshal( - prop_value, custom_formatters=custom_formatters) - - return properties diff --git a/openapi_core/schema/schemas/types.py b/openapi_core/schema/schemas/types.py index acfcae4..56f7445 100644 --- a/openapi_core/schema/schemas/types.py +++ b/openapi_core/schema/schemas/types.py @@ -1,6 +1,9 @@ import attr +NoValue = object() + + @attr.s(hash=True) class Contribution(object): src_prop_name = attr.ib() diff --git a/openapi_core/schema/schemas/unmarshallers.py b/openapi_core/schema/schemas/unmarshallers.py deleted file mode 100644 index f24188f..0000000 --- a/openapi_core/schema/schemas/unmarshallers.py +++ /dev/null @@ -1,101 +0,0 @@ -from six import text_type, binary_type, integer_types - -from openapi_core.schema.schemas.enums import SchemaFormat -from openapi_core.schema.schemas.exceptions import ( - InvalidCustomFormatSchemaValue, - UnmarshallerStrictTypeError, - FormatterNotFoundError, -) -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 FormatterNotFoundError(value, type_format) - - try: - return formatter(value) - except ValueError as exc: - raise InvalidCustomFormatSchemaValue(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, - } diff --git a/openapi_core/schema/schemas/util.py b/openapi_core/schema/schemas/util.py index 528b8fb..dcbf2fd 100644 --- a/openapi_core/schema/schemas/util.py +++ b/openapi_core/schema/schemas/util.py @@ -1,11 +1,7 @@ """OpenAPI core schemas util module""" -from base64 import b64decode -import datetime from distutils.util import strtobool +from six import string_types from json import dumps -from six import string_types, text_type, integer_types -import strict_rfc3339 -from uuid import UUID def forcebool(val): @@ -17,29 +13,3 @@ 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() - - -def format_datetime(value): - timestamp = strict_rfc3339.rfc3339_to_timestamp(value) - return datetime.datetime.utcfromtimestamp(timestamp) - - -def format_uuid(value): - if isinstance(value, UUID): - return value - return UUID(value) - - -def format_byte(value, encoding='utf8'): - return text_type(b64decode(value), encoding) - - -def format_number(value): - if isinstance(value, integer_types + (float, )): - return value - - return float(value) diff --git a/openapi_core/unmarshalling/__init__.py b/openapi_core/unmarshalling/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openapi_core/unmarshalling/schemas/__init__.py b/openapi_core/unmarshalling/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openapi_core/unmarshalling/schemas/exceptions.py b/openapi_core/unmarshalling/schemas/exceptions.py new file mode 100644 index 0000000..9b05745 --- /dev/null +++ b/openapi_core/unmarshalling/schemas/exceptions.py @@ -0,0 +1,66 @@ +import attr + +from openapi_core.exceptions import OpenAPIError + + +class UnmarshalError(OpenAPIError): + """Schema unmarshal operation error""" + pass + + +class UnmarshallerError(UnmarshalError): + """Unmarshaller error""" + pass + + +@attr.s(hash=True) +class UnmarshalValueError(UnmarshalError): + """Failed to unmarshal value to type""" + value = attr.ib() + type = attr.ib() + original_exception = attr.ib(default=None) + + def __str__(self): + return ( + "Failed to unmarshal value {value} to type {type}: {exception}" + ).format( + value=self.value, type=self.type, + exception=self.original_exception, + ) + + +@attr.s(hash=True) +class InvalidCustomFormatSchemaValue(UnmarshallerError): + """Value failed to format with custom formatter""" + value = attr.ib() + type = attr.ib() + original_exception = attr.ib() + + def __str__(self): + return ( + "Failed to format value {value} to format {type}: {exception}" + ).format( + value=self.value, type=self.type, + exception=self.original_exception, + ) + + +@attr.s(hash=True) +class FormatterNotFoundError(UnmarshallerError): + """Formatter not found to unmarshal""" + type_format = attr.ib() + + def __str__(self): + return "Formatter not found for {format} format".format( + format=self.type_format) + + +@attr.s(hash=True) +class UnmarshallerStrictTypeError(UnmarshallerError): + value = attr.ib() + types = attr.ib() + + def __str__(self): + types = ', '.join(list(map(str, self.types))) + return "Value {value} is not one of types: {types}".format( + value=self.value, types=types) diff --git a/openapi_core/unmarshalling/schemas/factories.py b/openapi_core/unmarshalling/schemas/factories.py new file mode 100644 index 0000000..ad11625 --- /dev/null +++ b/openapi_core/unmarshalling/schemas/factories.py @@ -0,0 +1,62 @@ +import warnings + +from openapi_core.schema.schemas.enums import SchemaType, SchemaFormat +from openapi_core.unmarshalling.schemas.exceptions import ( + FormatterNotFoundError, +) +from openapi_core.unmarshalling.schemas.unmarshallers import ( + StringUnmarshaller, IntegerUnmarshaller, NumberUnmarshaller, + BooleanUnmarshaller, ArrayUnmarshaller, ObjectUnmarshaller, + AnyUnmarshaller, +) + + +class SchemaUnmarshallersFactory(object): + + PRIMITIVE_UNMARSHALLERS = { + SchemaType.STRING: StringUnmarshaller, + SchemaType.INTEGER: IntegerUnmarshaller, + SchemaType.NUMBER: NumberUnmarshaller, + SchemaType.BOOLEAN: BooleanUnmarshaller, + } + COMPLEX_UNMARSHALLERS = { + SchemaType.ARRAY: ArrayUnmarshaller, + SchemaType.OBJECT: ObjectUnmarshaller, + SchemaType.ANY: AnyUnmarshaller, + } + + def __init__(self, custom_formatters=None): + if custom_formatters is None: + custom_formatters = {} + self.custom_formatters = custom_formatters + + def create(self, schema, type_override=None): + """Create unmarshaller from the schema.""" + if schema.deprecated: + warnings.warn("The schema is deprecated", DeprecationWarning) + + schema_type = type_override or schema.type + if schema_type in self.PRIMITIVE_UNMARSHALLERS: + klass = self.PRIMITIVE_UNMARSHALLERS[schema_type] + kwargs = dict(schema=schema) + + elif schema_type in self.COMPLEX_UNMARSHALLERS: + klass = self.COMPLEX_UNMARSHALLERS[schema_type] + kwargs = dict(schema=schema, unmarshallers_factory=self) + + formatter = self.get_formatter(klass.FORMATTERS, schema.format) + + if formatter is None: + raise FormatterNotFoundError(schema.format) + + return klass(formatter, **kwargs) + + def get_formatter(self, formatters, type_format=SchemaFormat.NONE): + try: + schema_format = SchemaFormat(type_format) + except ValueError: + return self.custom_formatters.get(type_format) + else: + if schema_format == SchemaFormat.NONE: + return lambda x: x + return formatters.get(schema_format) diff --git a/openapi_core/unmarshalling/schemas/unmarshallers.py b/openapi_core/unmarshalling/schemas/unmarshallers.py new file mode 100644 index 0000000..7817b5f --- /dev/null +++ b/openapi_core/unmarshalling/schemas/unmarshallers.py @@ -0,0 +1,240 @@ +import logging + +from six import text_type, binary_type, integer_types +from six import iteritems + +from openapi_core.extensions.models.factories import ModelFactory +from openapi_core.schema.schemas.enums import SchemaFormat, SchemaType +from openapi_core.schema.schemas.exceptions import ( + ValidateError, +) +from openapi_core.schema.schemas.types import NoValue +from openapi_core.unmarshalling.schemas.exceptions import ( + UnmarshalError, + InvalidCustomFormatSchemaValue, + UnmarshallerStrictTypeError, +) +from openapi_core.unmarshalling.schemas.util import ( + forcebool, format_date, format_datetime, format_byte, format_uuid, + format_number, +) + +log = logging.getLogger(__name__) + + +class StrictUnmarshaller(object): + + STRICT_TYPES = () + + def __call__(self, value, strict=True): + if strict and not self._is_strict(value): + raise UnmarshallerStrictTypeError(value, self.STRICT_TYPES) + + return value + + def _is_strict(self, value): + if not self.STRICT_TYPES: + return True + + return isinstance(value, self.STRICT_TYPES) + + +class PrimitiveTypeUnmarshaller(StrictUnmarshaller): + + FORMATTERS = {} + + def __init__(self, formatter, schema): + self.formatter = formatter + self.schema = schema + + def __call__(self, value=NoValue, strict=True): + if value is NoValue: + value = self.schema.default + if value is None: + return + value = super(PrimitiveTypeUnmarshaller, self).__call__( + value, strict=strict) + + return self.format(value) + + def format(self, value): + try: + return self.formatter(value) + except ValueError as exc: + raise InvalidCustomFormatSchemaValue( + value, self.schema.format, exc) + + +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, + } + + +class ComplexUnmarshaller(PrimitiveTypeUnmarshaller): + + def __init__(self, formatter, schema, unmarshallers_factory): + super(ComplexUnmarshaller, self).__init__(formatter, schema) + self.unmarshallers_factory = unmarshallers_factory + + +class ArrayUnmarshaller(ComplexUnmarshaller): + + STRICT_TYPES = (list, tuple) + FORMATTERS = {} + + @property + def items_unmarshaller(self): + return self.unmarshallers_factory.create(self.schema.items) + + def __call__(self, value=NoValue, strict=True): + value = super(ArrayUnmarshaller, self).__call__(value, strict=strict) + + self.unmarshallers_factory.create(self.schema.items) + + return list(map(self.items_unmarshaller, value)) + + +class ObjectUnmarshaller(ComplexUnmarshaller): + + STRICT_TYPES = (dict, ) + FORMATTERS = {} + + @property + def model_factory(self): + return ModelFactory() + + def __call__(self, value=NoValue, strict=True): + value = super(ObjectUnmarshaller, self).__call__(value, strict=strict) + + if self.schema.one_of: + properties = None + for one_of_schema in self.schema.one_of: + try: + unmarshalled = self._unmarshal_properties( + value, one_of_schema, strict=strict) + except (UnmarshalError, ValueError): + pass + else: + if properties is not None: + log.warning("multiple valid oneOf schemas found") + continue + properties = unmarshalled + + if properties is None: + log.warning("valid oneOf schema not found") + + else: + properties = self._unmarshal_properties(value) + + if 'x-model' in self.schema.extensions: + extension = self.schema.extensions['x-model'] + return self.model_factory.create(properties, name=extension.value) + + return properties + + def _unmarshal_properties( + self, value=NoValue, one_of_schema=None, strict=True): + all_props = self.schema.get_all_properties() + all_props_names = self.schema.get_all_properties_names() + + if one_of_schema is not None: + all_props.update(one_of_schema.get_all_properties()) + all_props_names |= one_of_schema.\ + get_all_properties_names() + + value_props_names = value.keys() + extra_props = set(value_props_names) - set(all_props_names) + + properties = {} + if self.schema.additional_properties is not True: + for prop_name in extra_props: + prop_value = value[prop_name] + properties[prop_name] = self.unmarshallers_factory.create( + self.schema.additional_properties)( + prop_value, strict=strict) + + for prop_name, prop in iteritems(all_props): + try: + prop_value = value[prop_name] + except KeyError: + if prop.default is NoValue: + continue + prop_value = prop.default + + properties[prop_name] = self.unmarshallers_factory.create( + prop)(prop_value, strict=strict) + + return properties + + +class AnyUnmarshaller(ComplexUnmarshaller): + + SCHEMA_TYPES_ORDER = [ + SchemaType.OBJECT, SchemaType.ARRAY, SchemaType.BOOLEAN, + SchemaType.INTEGER, SchemaType.NUMBER, SchemaType.STRING, + ] + + def __call__(self, value=NoValue, strict=True): + one_of_schema = self._get_one_of_schema(value) + if one_of_schema: + return self.unmarshallers_factory.create(one_of_schema)( + value, strict=strict) + + for schema_type in self.SCHEMA_TYPES_ORDER: + try: + unmarshaller = self.unmarshallers_factory.create( + self.schema, type_override=schema_type) + return unmarshaller(value, strict=strict) + except (UnmarshalError, ValueError): + continue + + log.warning("failed to unmarshal any type") + return value + + def _get_one_of_schema(self, value): + if not self.schema.one_of: + return + for subschema in self.schema.one_of: + try: + subschema.validate(value) + except ValidateError: + continue + else: + return subschema diff --git a/openapi_core/unmarshalling/schemas/util.py b/openapi_core/unmarshalling/schemas/util.py new file mode 100644 index 0000000..bdca71f --- /dev/null +++ b/openapi_core/unmarshalling/schemas/util.py @@ -0,0 +1,40 @@ +"""OpenAPI core schemas util module""" +from base64 import b64decode +import datetime +from distutils.util import strtobool +from six import string_types, text_type, integer_types +import strict_rfc3339 +from uuid import UUID + + +def forcebool(val): + if isinstance(val, string_types): + val = strtobool(val) + + return bool(val) + + +def format_date(value): + return datetime.datetime.strptime(value, '%Y-%m-%d').date() + + +def format_datetime(value): + timestamp = strict_rfc3339.rfc3339_to_timestamp(value) + return datetime.datetime.utcfromtimestamp(timestamp) + + +def format_uuid(value): + if isinstance(value, UUID): + return value + return UUID(value) + + +def format_byte(value, encoding='utf8'): + return text_type(b64decode(value), encoding) + + +def format_number(value): + if isinstance(value, integer_types + (float, )): + return value + + return float(value) diff --git a/openapi_core/validation/request/datatypes.py b/openapi_core/validation/request/datatypes.py index 27c9c1b..bb3e2a3 100644 --- a/openapi_core/validation/request/datatypes.py +++ b/openapi_core/validation/request/datatypes.py @@ -10,6 +10,18 @@ from six.moves.urllib.parse import urljoin @attr.s class RequestParameters(object): + """OpenAPI request parameters dataclass. + + Attributes: + path + Path parameters as dict. + query + Query string parameters as MultiDict. Must support getlist method. + header + Request headers as dict. + cookie + Request cookies as dict. + """ path = attr.ib(factory=dict) query = attr.ib(factory=ImmutableMultiDict) header = attr.ib(factory=dict) diff --git a/openapi_core/validation/request/validators.py b/openapi_core/validation/request/validators.py index f41008e..fc5c30a 100644 --- a/openapi_core/validation/request/validators.py +++ b/openapi_core/validation/request/validators.py @@ -7,7 +7,7 @@ from openapi_core.schema.media_types.exceptions import ( ) from openapi_core.schema.operations.exceptions import InvalidOperation from openapi_core.schema.parameters.exceptions import ( - OpenAPIParameterError, MissingRequiredParameter, + OpenAPIParameterError, MissingRequiredParameter, MissingParameter, ) from openapi_core.schema.paths.exceptions import InvalidPath from openapi_core.schema.request_bodies.exceptions import MissingRequestBody @@ -74,14 +74,16 @@ class RequestValidator(object): except MissingRequiredParameter as exc: errors.append(exc) continue - except OpenAPIParameterError: - continue - - try: - casted = param.cast(raw_value) - except OpenAPIParameterError as exc: - errors.append(exc) - continue + except MissingParameter: + if not param.schema or not param.schema.has_default(): + continue + casted = param.schema.default + else: + try: + casted = param.cast(raw_value) + except OpenAPIParameterError as exc: + errors.append(exc) + continue try: unmarshalled = param.unmarshal( diff --git a/tests/integration/data/v3.0/petstore.yaml b/tests/integration/data/v3.0/petstore.yaml index 241309a..c243bdc 100644 --- a/tests/integration/data/v3.0/petstore.yaml +++ b/tests/integration/data/v3.0/petstore.yaml @@ -278,6 +278,7 @@ components: $ref: "#/components/schemas/Pet" PetsData: type: object + x-model: PetsData required: - data properties: @@ -285,6 +286,7 @@ components: $ref: "#/components/schemas/Pets" PetData: type: object + x-model: PetData required: - data properties: @@ -292,6 +294,7 @@ components: $ref: "#/components/schemas/Pet" TagCreate: type: object + x-model: TagCreate required: - name properties: @@ -316,6 +319,7 @@ components: message: type: string ExtendedError: + x-model: ExtendedError allOf: - $ref: "#/components/schemas/Error" - type: object diff --git a/tests/unit/schema/test_schemas.py b/tests/unit/schema/test_schemas.py index 74fe396..e09c788 100644 --- a/tests/unit/schema/test_schemas.py +++ b/tests/unit/schema/test_schemas.py @@ -7,11 +7,16 @@ import pytest from openapi_core.extensions.models.models import Model from openapi_core.schema.schemas.enums import SchemaFormat, SchemaType from openapi_core.schema.schemas.exceptions import ( - InvalidSchemaValue, OpenAPISchemaError, UnmarshallerStrictTypeError, - UnmarshalValueError, UnmarshalError, InvalidCustomFormatSchemaValue, - FormatterNotFoundError, + InvalidSchemaValue, OpenAPISchemaError, ) from openapi_core.schema.schemas.models import Schema +from openapi_core.schema.schemas.types import NoValue +from openapi_core.unmarshalling.schemas.exceptions import ( + InvalidCustomFormatSchemaValue, + FormatterNotFoundError, + UnmarshalError, + UnmarshallerStrictTypeError, +) from six import b, u @@ -50,9 +55,8 @@ class TestSchemaUnmarshal(object): schema = Schema(schema_type) value = '' - result = schema.unmarshal(value) - - assert result is None + with pytest.raises(UnmarshallerStrictTypeError): + schema.unmarshal(value) def test_string_valid(self): schema = Schema('string') @@ -93,25 +97,19 @@ class TestSchemaUnmarshal(object): with pytest.raises(UnmarshallerStrictTypeError): schema.unmarshal(value) - def test_string_none(self): - schema = Schema('string') - value = None - - with pytest.raises(UnmarshalError): - schema.unmarshal(value) - def test_string_default(self): default_value = 'default' schema = Schema('string', default=default_value) - value = None + value = NoValue - with pytest.raises(UnmarshalError): - schema.unmarshal(value) + result = schema.unmarshal(value) - def test_string_default_nullable(self): - default_value = 'default' + assert result == default_value + + @pytest.mark.parametrize('default_value', ['default', None]) + def test_string_default_nullable(self, default_value): schema = Schema('string', default=default_value, nullable=True) - value = None + value = NoValue result = schema.unmarshal(value) @@ -161,7 +159,7 @@ class TestSchemaUnmarshal(object): schema = Schema('string', schema_format=unknown_format) value = 'x' - with pytest.raises(OpenAPISchemaError): + with pytest.raises(FormatterNotFoundError): schema.unmarshal(value) def test_string_format_invalid_value(self): @@ -172,7 +170,7 @@ class TestSchemaUnmarshal(object): with pytest.raises( FormatterNotFoundError, message=( - 'Formatter not found for custom format to unmarshal value x' + 'Formatter not found for custom format' ), ): schema.unmarshal(value) @@ -215,21 +213,22 @@ class TestSchemaUnmarshal(object): schema.unmarshal(value) def test_integer_default(self): - default_value = '123' + default_value = 123 schema = Schema('integer', default=default_value) - value = None + value = NoValue - with pytest.raises(UnmarshalError): - schema.unmarshal(value) + result = schema.unmarshal(value) + + assert result == default_value def test_integer_default_nullable(self): - default_value = '123' + default_value = 123 schema = Schema('integer', default=default_value, nullable=True) value = None result = schema.unmarshal(value) - assert result == default_value + assert result is None def test_integer_invalid(self): schema = Schema('integer') @@ -250,14 +249,14 @@ class TestSchemaUnmarshal(object): schema = Schema('array', items=Schema('string')) value = '123' - with pytest.raises(UnmarshalValueError): + with pytest.raises(UnmarshallerStrictTypeError): schema.unmarshal(value) def test_array_of_integer_string_invalid(self): schema = Schema('array', items=Schema('integer')) value = '123' - with pytest.raises(UnmarshalValueError): + with pytest.raises(UnmarshallerStrictTypeError): schema.unmarshal(value) def test_boolean_valid(self): @@ -702,7 +701,7 @@ class TestSchemaValidate(object): unknown_format = 'unknown' schema = Schema('string', schema_format=unknown_format) - with pytest.raises(OpenAPISchemaError): + with pytest.raises(InvalidSchemaValue): schema.validate(value) @pytest.mark.parametrize('value', [u(""), u("a"), u("ab")])