From 64628d1cc9e346896c64b2eaf199c2adc02fff12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Domen=20Ko=C5=BEar?= Date: Fri, 24 Aug 2018 15:57:41 +0100 Subject: [PATCH 1/7] Sketch out custom formatters design --- openapi_core/schema/media_types/models.py | 4 +- openapi_core/schema/parameters/models.py | 4 +- openapi_core/schema/schemas/models.py | 68 +++++++++++-------- openapi_core/validation/request/validators.py | 7 +- .../validation/response/validators.py | 5 +- requirements.txt | 1 + requirements_2.7.txt | 1 + 7 files changed, 52 insertions(+), 38 deletions(-) diff --git a/openapi_core/schema/media_types/models.py b/openapi_core/schema/media_types/models.py index d47e696..760587f 100644 --- a/openapi_core/schema/media_types/models.py +++ b/openapi_core/schema/media_types/models.py @@ -32,7 +32,7 @@ class MediaType(object): deserializer = self.get_dererializer() return deserializer(value) - def unmarshal(self, value): + def unmarshal(self, value, custom_formatters=None): if not self.schema: return value @@ -42,7 +42,7 @@ class MediaType(object): raise InvalidMediaTypeValue(str(exc)) try: - unmarshalled = self.schema.unmarshal(deserialized) + unmarshalled = self.schema.unmarshal(deserialized, custom_formatters) except InvalidSchemaValue as exc: raise InvalidMediaTypeValue(str(exc)) diff --git a/openapi_core/schema/parameters/models.py b/openapi_core/schema/parameters/models.py index 7ca6657..c515128 100644 --- a/openapi_core/schema/parameters/models.py +++ b/openapi_core/schema/parameters/models.py @@ -91,7 +91,7 @@ class Parameter(object): return location[self.name] - def unmarshal(self, value): + def unmarshal(self, value, custom_formatters=None): if self.deprecated: warnings.warn( "{0} parameter is deprecated".format(self.name), @@ -112,7 +112,7 @@ class Parameter(object): raise InvalidParameterValue(str(exc)) try: - unmarshalled = self.schema.unmarshal(deserialized) + unmarshalled = self.schema.unmarshal(deserialized, custom_formatters) except InvalidSchemaValue as exc: raise InvalidParameterValue(str(exc)) diff --git a/openapi_core/schema/schemas/models.py b/openapi_core/schema/schemas/models.py index c0014c5..31139c7 100644 --- a/openapi_core/schema/schemas/models.py +++ b/openapi_core/schema/schemas/models.py @@ -1,4 +1,6 @@ """OpenAPI core schemas models module""" +import attr +import functools import logging from collections import defaultdict from datetime import date, datetime @@ -23,6 +25,11 @@ from openapi_core.schema.schemas.validators import ( log = logging.getLogger(__name__) +@attr.s +class StringFormat(object): + format = attr.ib() + validate = attr.ib() + class Schema(object): """Represents an OpenAPI Schema.""" @@ -33,18 +40,11 @@ class Schema(object): SchemaType.BOOLEAN: forcebool, } - 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), + STRING_FORMAT_CALLABLE_GETTER = { + SchemaFormat.NONE: StringFormat(text_type, TypeValidator(text_type)), + SchemaFormat.DATE: StringFormat(format_date, TypeValidator(date, exclude=datetime)), + SchemaFormat.DATETIME: StringFormat(format_datetime, TypeValidator(datetime)), + SchemaFormat.BINARY: StringFormat(binary_type, TypeValidator(binary_type)), } TYPE_VALIDATOR_CALLABLE_GETTER = { @@ -99,6 +99,7 @@ class Schema(object): self._all_required_properties_cache = None self._all_optional_properties_cache = None + self.custom_formatters = None def __getitem__(self, name): return self.properties[name] @@ -173,11 +174,13 @@ class Schema(object): "Failed to cast value of {0} to {1}".format(value, self.type) ) - def unmarshal(self, value): + def unmarshal(self, value, custom_formatters=None): """Unmarshal parameter from the value.""" if self.deprecated: warnings.warn("The schema is deprecated", DeprecationWarning) + self.custom_formatters = custom_formatters + casted = self.cast(value) if casted is None and not self.required: @@ -195,15 +198,18 @@ class Schema(object): try: schema_format = SchemaFormat(self.format) except ValueError: - # @todo: implement custom format unmarshalling support - raise OpenAPISchemaError( - "Unsupported {0} format unmarshalling".format(self.format) - ) + msg = "Unsupported {0} format unmarshalling".format(self.format) + if self.custom_formatters is not None: + formatstring = self.custom_formatters.get(self.format) + if formatstring is None: + raise OpenAPISchemaError(msg) + else: + raise OpenAPISchemaError(msg) else: - formatter = self.STRING_FORMAT_CAST_CALLABLE_GETTER[schema_format] + formatstring = self.STRING_FORMAT_CALLABLE_GETTER[schema_format] try: - return formatter(value) + return formatstring.format(value) except ValueError: raise InvalidSchemaValue( "Failed to format value of {0} to {1}".format( @@ -231,7 +237,8 @@ class Schema(object): if self.items is None: raise UndefinedItemsSchema("Undefined items' schema") - return list(map(self.items.unmarshal, value)) + f = functools.partial(self.items.unmarshal, custom_formatters=self.custom_formatters) + return list(map(f, value)) def _unmarshal_object(self, value, model_factory=None): if not isinstance(value, (dict, )): @@ -286,7 +293,7 @@ class Schema(object): for prop_name in extra_props: prop_value = value[prop_name] properties[prop_name] = self.additional_properties.unmarshal( - prop_value) + prop_value, self.custom_formatters) for prop_name, prop in iteritems(all_props): try: @@ -298,7 +305,7 @@ class Schema(object): if not prop.nullable and not prop.default: continue prop_value = prop.default - properties[prop_name] = prop.unmarshal(prop_value) + properties[prop_name] = prop.unmarshal(prop_value, self.custom_formatters) self._validate_properties(properties, one_of_schema=one_of_schema) @@ -405,15 +412,18 @@ class Schema(object): try: schema_format = SchemaFormat(self.format) except ValueError: - # @todo: implement custom format validation support - raise OpenAPISchemaError( - "Unsupported {0} format validation".format(self.format) - ) + msg = "Unsupported {0} format validation".format(self.format) + if self.custom_formatters is not None: + formatstring = self.custom_formatters.get(self.format) + if formatstring is None: + raise OpenAPISchemaError(msg) + else: + raise OpenAPISchemaError(msg) else: - format_validator_callable =\ - self.STRING_FORMAT_VALIDATOR_CALLABLE_GETTER[schema_format] + formatstring =\ + self.STRING_FORMAT_CALLABLE_GETTER[schema_format] - if not format_validator_callable(value): + if not formatstring.validate(value): raise InvalidSchemaValue( "Value of {0} not valid format of {1}".format( value, self.format) diff --git a/openapi_core/validation/request/validators.py b/openapi_core/validation/request/validators.py index 87b638a..08593f6 100644 --- a/openapi_core/validation/request/validators.py +++ b/openapi_core/validation/request/validators.py @@ -11,8 +11,9 @@ from openapi_core.validation.util import get_operation_pattern class RequestValidator(object): - def __init__(self, spec): + def __init__(self, spec, custom_formatters=None): self.spec = spec + self.custom_formatters = custom_formatters def validate(self, request): try: @@ -52,7 +53,7 @@ class RequestValidator(object): continue try: - value = param.unmarshal(raw_value) + value = param.unmarshal(raw_value, self.custom_formatters) except OpenAPIMappingError as exc: errors.append(exc) else: @@ -78,7 +79,7 @@ class RequestValidator(object): errors.append(exc) else: try: - body = media_type.unmarshal(raw_body) + body = media_type.unmarshal(raw_body, self.custom_formatters) except OpenAPIMappingError as exc: errors.append(exc) diff --git a/openapi_core/validation/response/validators.py b/openapi_core/validation/response/validators.py index b926fb8..4f9696b 100644 --- a/openapi_core/validation/response/validators.py +++ b/openapi_core/validation/response/validators.py @@ -6,8 +6,9 @@ from openapi_core.validation.util import get_operation_pattern class ResponseValidator(object): - def __init__(self, spec): + def __init__(self, spec, custom_formatters=None): self.spec = spec + self.custom_formatters = custom_formatters def validate(self, request, response): try: @@ -60,7 +61,7 @@ class ResponseValidator(object): errors.append(exc) else: try: - data = media_type.unmarshal(raw_data) + data = media_type.unmarshal(raw_data, self.custom_formatters) except OpenAPIMappingError as exc: errors.append(exc) diff --git a/requirements.txt b/requirements.txt index f13ec83..a5b8a67 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ openapi-spec-validator six lazy-object-proxy +attrs diff --git a/requirements_2.7.txt b/requirements_2.7.txt index 19ba7bb..97f5124 100644 --- a/requirements_2.7.txt +++ b/requirements_2.7.txt @@ -4,3 +4,4 @@ lazy-object-proxy backports.functools-lru-cache backports.functools-partialmethod enum34 +attrs From 89a53f6edcd59cab69fcf81dfb36ab003b481b5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Domen=20Ko=C5=BEar?= Date: Wed, 5 Sep 2018 12:39:10 +0100 Subject: [PATCH 2/7] review feedback --- openapi_core/schema/media_types/models.py | 4 +- openapi_core/schema/parameters/models.py | 4 +- openapi_core/schema/schemas/models.py | 105 ++++++++++++---------- setup.py | 1 + 4 files changed, 65 insertions(+), 49 deletions(-) diff --git a/openapi_core/schema/media_types/models.py b/openapi_core/schema/media_types/models.py index 760587f..e2b0d2f 100644 --- a/openapi_core/schema/media_types/models.py +++ b/openapi_core/schema/media_types/models.py @@ -42,11 +42,11 @@ class MediaType(object): raise InvalidMediaTypeValue(str(exc)) try: - unmarshalled = self.schema.unmarshal(deserialized, custom_formatters) + unmarshalled = self.schema.unmarshal(deserialized, custom_formatters=custom_formatters) except InvalidSchemaValue as exc: raise InvalidMediaTypeValue(str(exc)) try: - return self.schema.validate(unmarshalled) + return self.schema.validate(unmarshalled, custom_formatters=custom_formatters) except InvalidSchemaValue as exc: raise InvalidMediaTypeValue(str(exc)) diff --git a/openapi_core/schema/parameters/models.py b/openapi_core/schema/parameters/models.py index c515128..e484ce1 100644 --- a/openapi_core/schema/parameters/models.py +++ b/openapi_core/schema/parameters/models.py @@ -112,11 +112,11 @@ class Parameter(object): raise InvalidParameterValue(str(exc)) try: - unmarshalled = self.schema.unmarshal(deserialized, custom_formatters) + unmarshalled = self.schema.unmarshal(deserialized, custom_formatters=custom_formatters) except InvalidSchemaValue as exc: raise InvalidParameterValue(str(exc)) try: - return self.schema.validate(unmarshalled) + return self.schema.validate(unmarshalled, custom_formatters=custom_formatters) except InvalidSchemaValue as exc: raise InvalidParameterValue(str(exc)) diff --git a/openapi_core/schema/schemas/models.py b/openapi_core/schema/schemas/models.py index 31139c7..d5ff5b7 100644 --- a/openapi_core/schema/schemas/models.py +++ b/openapi_core/schema/schemas/models.py @@ -25,9 +25,10 @@ from openapi_core.schema.schemas.validators import ( log = logging.getLogger(__name__) + @attr.s -class StringFormat(object): - format = attr.ib() +class Format(object): + unmarshal = attr.ib() validate = attr.ib() @@ -41,10 +42,10 @@ class Schema(object): } STRING_FORMAT_CALLABLE_GETTER = { - SchemaFormat.NONE: StringFormat(text_type, TypeValidator(text_type)), - SchemaFormat.DATE: StringFormat(format_date, TypeValidator(date, exclude=datetime)), - SchemaFormat.DATETIME: StringFormat(format_datetime, TypeValidator(datetime)), - SchemaFormat.BINARY: StringFormat(binary_type, TypeValidator(binary_type)), + SchemaFormat.NONE: Format(text_type, TypeValidator(text_type)), + SchemaFormat.DATE: Format(format_date, TypeValidator(date, exclude=datetime)), + SchemaFormat.DATETIME: Format(format_datetime, TypeValidator(datetime)), + SchemaFormat.BINARY: Format(binary_type, TypeValidator(binary_type)), } TYPE_VALIDATOR_CALLABLE_GETTER = { @@ -99,7 +100,6 @@ class Schema(object): self._all_required_properties_cache = None self._all_optional_properties_cache = None - self.custom_formatters = None def __getitem__(self, name): return self.properties[name] @@ -143,25 +143,27 @@ class Schema(object): return set(required) - def get_cast_mapping(self): + def get_cast_mapping(self, custom_formatters=None): + pass_defaults = lambda f: functools.partial( + f, custom_formatters=custom_formatters) mapping = self.DEFAULT_CAST_CALLABLE_GETTER.copy() mapping.update({ - SchemaType.STRING: self._unmarshal_string, - SchemaType.ANY: self._unmarshal_any, - SchemaType.ARRAY: self._unmarshal_collection, - SchemaType.OBJECT: self._unmarshal_object, + SchemaType.STRING: pass_defaults(self._unmarshal_string), + 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 cast(self, value): + def cast(self, value, custom_formatters=None): """Cast value to schema type""" if value is None: if not self.nullable: raise InvalidSchemaValue("Null value for non-nullable schema") return self.default - cast_mapping = self.get_cast_mapping() + cast_mapping = self.get_cast_mapping(custom_formatters=custom_formatters) if self.type is not SchemaType.STRING and value == '': return None @@ -179,9 +181,7 @@ class Schema(object): if self.deprecated: warnings.warn("The schema is deprecated", DeprecationWarning) - self.custom_formatters = custom_formatters - - casted = self.cast(value) + casted = self.cast(value, custom_formatters=custom_formatters) if casted is None and not self.required: return None @@ -194,13 +194,13 @@ class Schema(object): return casted - def _unmarshal_string(self, value): + def _unmarshal_string(self, value, custom_formatters=None): try: schema_format = SchemaFormat(self.format) except ValueError: msg = "Unsupported {0} format unmarshalling".format(self.format) - if self.custom_formatters is not None: - formatstring = self.custom_formatters.get(self.format) + if custom_formatters is not None: + formatstring = custom_formatters.get(self.format) if formatstring is None: raise OpenAPISchemaError(msg) else: @@ -209,14 +209,14 @@ class Schema(object): formatstring = self.STRING_FORMAT_CALLABLE_GETTER[schema_format] try: - return formatstring.format(value) + return formatstring.unmarshal(value) except ValueError: raise InvalidSchemaValue( "Failed to format value of {0} to {1}".format( value, self.format) ) - def _unmarshal_any(self, value): + def _unmarshal_any(self, value, custom_formatters=None): types_resolve_order = [ SchemaType.OBJECT, SchemaType.ARRAY, SchemaType.BOOLEAN, SchemaType.INTEGER, SchemaType.NUMBER, SchemaType.STRING, @@ -233,14 +233,16 @@ class Schema(object): raise NoValidSchema( "No valid schema found for value {0}".format(value)) - def _unmarshal_collection(self, value): + def _unmarshal_collection(self, value, custom_formatters=None): if self.items is None: raise UndefinedItemsSchema("Undefined items' schema") - f = functools.partial(self.items.unmarshal, custom_formatters=self.custom_formatters) + f = functools.partial(self.items.unmarshal, + custom_formatters=custom_formatters) return list(map(f, value)) - def _unmarshal_object(self, value, model_factory=None): + def _unmarshal_object(self, value, model_factory=None, + custom_formatters=None): if not isinstance(value, (dict, )): raise InvalidSchemaValue( "Value of {0} not a dict".format(value)) @@ -252,7 +254,7 @@ class Schema(object): for one_of_schema in self.one_of: try: found_props = self._unmarshal_properties( - value, one_of_schema) + value, one_of_schema, custom_formatters=custom_formatters) except OpenAPISchemaError: pass else: @@ -267,11 +269,13 @@ class Schema(object): "Exactly one valid schema should be valid, None found.") else: - properties = self._unmarshal_properties(value) + 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): + def _unmarshal_properties(self, value, one_of_schema=None, + custom_formatters=None): all_props = self.get_all_properties() all_props_names = self.get_all_properties_names() all_req_props_names = self.get_all_required_properties_names() @@ -293,7 +297,7 @@ class Schema(object): for prop_name in extra_props: prop_value = value[prop_name] properties[prop_name] = self.additional_properties.unmarshal( - prop_value, self.custom_formatters) + prop_value, custom_formatters=custom_formatters) for prop_name, prop in iteritems(all_props): try: @@ -305,9 +309,11 @@ class Schema(object): if not prop.nullable and not prop.default: continue prop_value = prop.default - properties[prop_name] = prop.unmarshal(prop_value, self.custom_formatters) + properties[prop_name] = prop.unmarshal( + prop_value, custom_formatters=custom_formatters) - self._validate_properties(properties, one_of_schema=one_of_schema) + self._validate_properties(properties, one_of_schema=one_of_schema, + custom_formatters=custom_formatters) return properties @@ -320,9 +326,12 @@ class Schema(object): SchemaType.NUMBER: self._validate_number, } - return defaultdict(lambda: lambda x: x, mapping) + def default(x, **kw): + return x - def validate(self, value): + return defaultdict(lambda: default, mapping) + + def validate(self, value, custom_formatters=None): if value is None: if not self.nullable: raise InvalidSchemaValue("Null value for non-nullable schema") @@ -340,11 +349,11 @@ class Schema(object): # structure validation validator_mapping = self.get_validator_mapping() validator_callable = validator_mapping[self.type] - validator_callable(value) + validator_callable(value, custom_formatters=custom_formatters) return value - def _validate_collection(self, value): + def _validate_collection(self, value, custom_formatters=None): if self.items is None: raise OpenAPISchemaError("Schema for collection not defined") @@ -375,7 +384,9 @@ class Schema(object): if self.unique_items and len(set(value)) != len(value): raise InvalidSchemaValue("Value may not contain duplicate items") - return list(map(self.items.validate, value)) + f = functools.partial(self.items.validate, + custom_formatters=custom_formatters) + return list(map(f, value)) def _validate_number(self, value): if self.minimum is not None: @@ -408,13 +419,13 @@ class Schema(object): value, self.multiple_of) ) - def _validate_string(self, value): + def _validate_string(self, value, custom_formatters=None): try: schema_format = SchemaFormat(self.format) except ValueError: msg = "Unsupported {0} format validation".format(self.format) - if self.custom_formatters is not None: - formatstring = self.custom_formatters.get(self.format) + if custom_formatters is not None: + formatstring = custom_formatters.get(self.format) if formatstring is None: raise OpenAPISchemaError(msg) else: @@ -459,14 +470,16 @@ class Schema(object): return True - def _validate_object(self, value): + def _validate_object(self, value, custom_formatters=None): properties = value.__dict__ if self.one_of: valid_one_of_schema = None for one_of_schema in self.one_of: try: - self._validate_properties(properties, one_of_schema) + self._validate_properties( + properties, one_of_schema, + custom_formatters=custom_formatters) except OpenAPISchemaError: pass else: @@ -481,7 +494,8 @@ class Schema(object): "Exactly one valid schema should be valid, None found.") else: - self._validate_properties(properties) + self._validate_properties(properties, + custom_formatters=custom_formatters) if self.min_properties is not None: if self.min_properties < 0: @@ -512,7 +526,8 @@ class Schema(object): return True - def _validate_properties(self, value, one_of_schema=None): + def _validate_properties(self, value, one_of_schema=None, + custom_formatters=None): all_props = self.get_all_properties() all_props_names = self.get_all_properties_names() all_req_props_names = self.get_all_required_properties_names() @@ -533,7 +548,7 @@ class Schema(object): for prop_name in extra_props: prop_value = value[prop_name] self.additional_properties.validate( - prop_value) + prop_value, custom_formatters=custom_formatters) for prop_name, prop in iteritems(all_props): try: @@ -545,6 +560,6 @@ class Schema(object): if not prop.nullable and not prop.default: continue prop_value = prop.default - prop.validate(prop_value) + prop.validate(prop_value, custom_formatters=custom_formatters) return True diff --git a/setup.py b/setup.py index 483c4f9..42ea7dc 100644 --- a/setup.py +++ b/setup.py @@ -44,6 +44,7 @@ class PyTest(TestCommand): '--cov', 'openapi_core', '--cov-report', 'term-missing', '--cov-report', 'xml:reports/coverage.xml', + 'tests', ] self.test_suite = True From 6bdd1a6756884f104ef5c70f570442444a6f01b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Domen=20Ko=C5=BEar?= Date: Thu, 6 Sep 2018 15:50:16 +0100 Subject: [PATCH 3/7] Rewrok exception handling Main motivation behind this change is to be able to catch exceptions as per raise_for_errors() helpers, but to inspect state of exceptions instead of just getting a rendered string. This allows rendering exceptions into JSON, for example. --- openapi_core/schema/content/exceptions.py | 9 ++- openapi_core/schema/content/models.py | 2 +- openapi_core/schema/media_types/exceptions.py | 14 +++- openapi_core/schema/media_types/models.py | 6 +- openapi_core/schema/operations/exceptions.py | 10 ++- openapi_core/schema/operations/models.py | 3 +- openapi_core/schema/parameters/exceptions.py | 27 +++++-- openapi_core/schema/parameters/models.py | 15 ++-- .../schema/request_bodies/exceptions.py | 8 ++- openapi_core/schema/request_bodies/models.py | 6 +- openapi_core/schema/responses/exceptions.py | 15 +++- openapi_core/schema/responses/models.py | 5 +- openapi_core/schema/schemas/exceptions.py | 53 ++++++++++++-- openapi_core/schema/schemas/models.py | 71 +++++++------------ openapi_core/schema/servers/exceptions.py | 9 ++- openapi_core/schema/specs/models.py | 7 +- 16 files changed, 167 insertions(+), 93 deletions(-) diff --git a/openapi_core/schema/content/exceptions.py b/openapi_core/schema/content/exceptions.py index 88ca440..4473ef0 100644 --- a/openapi_core/schema/content/exceptions.py +++ b/openapi_core/schema/content/exceptions.py @@ -1,9 +1,16 @@ from openapi_core.schema.exceptions import OpenAPIMappingError +import attr + class OpenAPIContentError(OpenAPIMappingError): pass +@attr.s class MimeTypeNotFound(OpenAPIContentError): - pass + mimetype = attr.ib() + availableMimetypes = attr.ib() + + def __str__(self): + return "Mimetype not found: {0}. Valid mimetypes: {1}".format(self.mimetype, self.availableMimetypes) diff --git a/openapi_core/schema/content/models.py b/openapi_core/schema/content/models.py index 1173671..1cabec1 100644 --- a/openapi_core/schema/content/models.py +++ b/openapi_core/schema/content/models.py @@ -18,4 +18,4 @@ class Content(dict): if fnmatch.fnmatch(mimetype, key): return value - raise MimeTypeNotFound("{0} mimetype not found") + raise MimeTypeNotFound(mimetype, self.keys()) diff --git a/openapi_core/schema/media_types/exceptions.py b/openapi_core/schema/media_types/exceptions.py index 293f438..97a0d85 100644 --- a/openapi_core/schema/media_types/exceptions.py +++ b/openapi_core/schema/media_types/exceptions.py @@ -1,13 +1,21 @@ from openapi_core.schema.exceptions import OpenAPIMappingError +import attr + class OpenAPIMediaTypeError(OpenAPIMappingError): pass - +@attr.s class InvalidMediaTypeValue(OpenAPIMediaTypeError): - pass + original_exception = attr.ib() + def __str__(self): + return "Mimetype invalid: {0}".format(self.original_exception) +@attr.s class InvalidContentType(OpenAPIMediaTypeError): - pass + mimetype = attr.ib() + + def __str__(self): + return "Content for following mimetype not found: {0}".format(self.mimetype) diff --git a/openapi_core/schema/media_types/models.py b/openapi_core/schema/media_types/models.py index e2b0d2f..8635bdf 100644 --- a/openapi_core/schema/media_types/models.py +++ b/openapi_core/schema/media_types/models.py @@ -39,14 +39,14 @@ class MediaType(object): try: deserialized = self.deserialize(value) except ValueError as exc: - raise InvalidMediaTypeValue(str(exc)) + raise InvalidMediaTypeValue(exc) try: unmarshalled = self.schema.unmarshal(deserialized, custom_formatters=custom_formatters) except InvalidSchemaValue as exc: - raise InvalidMediaTypeValue(str(exc)) + raise InvalidMediaTypeValue(exc) try: return self.schema.validate(unmarshalled, custom_formatters=custom_formatters) except InvalidSchemaValue as exc: - raise InvalidMediaTypeValue(str(exc)) + raise InvalidMediaTypeValue(exc) diff --git a/openapi_core/schema/operations/exceptions.py b/openapi_core/schema/operations/exceptions.py index b49d189..30118b6 100644 --- a/openapi_core/schema/operations/exceptions.py +++ b/openapi_core/schema/operations/exceptions.py @@ -1,9 +1,17 @@ from openapi_core.schema.exceptions import OpenAPIMappingError +import attr + class OpenAPIOperationError(OpenAPIMappingError): pass +@attr.s class InvalidOperation(OpenAPIOperationError): - pass + path_pattern = attr.ib() + http_method = attr.ib() + + def __str__(self): + return "Unknown operation path {0} with method {1}".format( + self.path_pattern, self.http_method) diff --git a/openapi_core/schema/operations/models.py b/openapi_core/schema/operations/models.py index ab772fb..91e64cd 100644 --- a/openapi_core/schema/operations/models.py +++ b/openapi_core/schema/operations/models.py @@ -32,7 +32,6 @@ class Operation(object): return self.responses[http_status_range] if 'default' not in self.responses: - raise InvalidResponse( - "Unknown response http status {0}".format(http_status)) + raise InvalidResponse(http_status, self.responses) return self.responses['default'] diff --git a/openapi_core/schema/parameters/exceptions.py b/openapi_core/schema/parameters/exceptions.py index 7fa76e0..c159301 100644 --- a/openapi_core/schema/parameters/exceptions.py +++ b/openapi_core/schema/parameters/exceptions.py @@ -1,21 +1,40 @@ from openapi_core.schema.exceptions import OpenAPIMappingError +import attr + class OpenAPIParameterError(OpenAPIMappingError): pass +@attr.s class MissingParameter(OpenAPIParameterError): - pass + name = attr.ib() + + def __str__(self): + return "Missing parameter (without default value): {0}".format(self.name) +@attr.s class MissingRequiredParameter(OpenAPIParameterError): - pass + name = attr.ib() + + def __str__(self): + return "Missing required parameter: {0}".format(self.name) +@attr.s class EmptyParameterValue(OpenAPIParameterError): - pass + name = attr.ib() + + def __str__(self): + return "Value of parameter cannot be empty: {0}".format(self.name) +@attr.s class InvalidParameterValue(OpenAPIParameterError): - pass + name = attr.ib() + original_exception = attr.ib() + + def __str__(self): + return "Invalid parameter value for `{0}`: {1}".format(sef.name, self.original_exception) diff --git a/openapi_core/schema/parameters/models.py b/openapi_core/schema/parameters/models.py index e484ce1..dcc8fd7 100644 --- a/openapi_core/schema/parameters/models.py +++ b/openapi_core/schema/parameters/models.py @@ -77,12 +77,10 @@ class Parameter(object): if self.name not in location: if self.required: - raise MissingRequiredParameter( - "Missing required `{0}` parameter".format(self.name)) + raise MissingRequiredParameter(self.name) if not self.schema or self.schema.default is None: - raise MissingParameter( - "Missing `{0}` parameter".format(self.name)) + raise MissingParameter(self.name) return self.schema.default @@ -100,8 +98,7 @@ class Parameter(object): if (self.location == ParameterLocation.QUERY and value == "" and not self.allow_empty_value): - raise EmptyParameterValue( - "Value of {0} parameter cannot be empty".format(self.name)) + raise EmptyParameterValue(self.name) if not self.schema: return value @@ -109,14 +106,14 @@ class Parameter(object): try: deserialized = self.deserialize(value) except (ValueError, AttributeError) as exc: - raise InvalidParameterValue(str(exc)) + raise InvalidParameterValue(self.name, exc) try: unmarshalled = self.schema.unmarshal(deserialized, custom_formatters=custom_formatters) except InvalidSchemaValue as exc: - raise InvalidParameterValue(str(exc)) + raise InvalidParameterValue(self.name, exc) try: return self.schema.validate(unmarshalled, custom_formatters=custom_formatters) except InvalidSchemaValue as exc: - raise InvalidParameterValue(str(exc)) + raise InvalidParameterValue(self.name, exc) diff --git a/openapi_core/schema/request_bodies/exceptions.py b/openapi_core/schema/request_bodies/exceptions.py index 0e07998..3b0e618 100644 --- a/openapi_core/schema/request_bodies/exceptions.py +++ b/openapi_core/schema/request_bodies/exceptions.py @@ -1,9 +1,15 @@ from openapi_core.schema.exceptions import OpenAPIMappingError +import attr + class OpenAPIRequestBodyError(OpenAPIMappingError): pass +@attr.s class MissingRequestBody(OpenAPIRequestBodyError): - pass + request = attr.ib() + + def __str__(self): + return "Missing required request body" diff --git a/openapi_core/schema/request_bodies/models.py b/openapi_core/schema/request_bodies/models.py index c8f775c..07b966f 100644 --- a/openapi_core/schema/request_bodies/models.py +++ b/openapi_core/schema/request_bodies/models.py @@ -16,11 +16,9 @@ class RequestBody(object): try: return self.content[mimetype] except MimeTypeNotFound: - raise InvalidContentType( - "Invalid mime type `{0}`".format(mimetype)) + raise InvalidContentType(mimetype) def get_value(self, request): if not request.body and self.required: - raise MissingRequestBody("Missing required request body") - + raise MissingRequestBody(request) return request.body diff --git a/openapi_core/schema/responses/exceptions.py b/openapi_core/schema/responses/exceptions.py index 01ba373..5e196e6 100644 --- a/openapi_core/schema/responses/exceptions.py +++ b/openapi_core/schema/responses/exceptions.py @@ -1,13 +1,24 @@ from openapi_core.schema.exceptions import OpenAPIMappingError +import attr + class OpenAPIResponseError(OpenAPIMappingError): pass +@attr.s class InvalidResponse(OpenAPIResponseError): - pass + http_status = attr.ib() + responses = attr.ib() + + def __str__(self): + return "Unknown response http status: {0}".format(str(self.http_status)) +@attr.s class MissingResponseContent(OpenAPIResponseError): - pass + response = attr.ib() + + def __str__(self): + return "Missing response content" diff --git a/openapi_core/schema/responses/models.py b/openapi_core/schema/responses/models.py index 530e0c4..a16f9e4 100644 --- a/openapi_core/schema/responses/models.py +++ b/openapi_core/schema/responses/models.py @@ -23,11 +23,10 @@ class Response(object): try: return self.content[mimetype] except MimeTypeNotFound: - raise InvalidContentType( - "Invalid mime type `{0}`".format(mimetype)) + raise InvalidContentType(mimetype) def get_value(self, response): if not response.data: - raise MissingResponseContent("Missing response content") + raise MissingResponseContent(response) return response.data diff --git a/openapi_core/schema/schemas/exceptions.py b/openapi_core/schema/schemas/exceptions.py index ef51d21..d8d38e4 100644 --- a/openapi_core/schema/schemas/exceptions.py +++ b/openapi_core/schema/schemas/exceptions.py @@ -1,33 +1,72 @@ from openapi_core.schema.exceptions import OpenAPIMappingError +import attr + class OpenAPISchemaError(OpenAPIMappingError): pass +@attr.s class NoValidSchema(OpenAPISchemaError): - pass + value = attr.ib() + + def __str__(self): + return "No valid schema found for value: {0}".format(self.value) +@attr.s class UndefinedItemsSchema(OpenAPISchemaError): - pass + type = attr.ib() + + def __str__(self): + return "Null value for schema type {0}".format(self.type) +@attr.s class InvalidSchemaValue(OpenAPISchemaError): - pass + msg = attr.ib() + value = attr.ib() + type = attr.ib() + + def __str__(self): + return self.msg.format(value=self.value, type=self.type) + +@attr.s +class InvalidCustomFormatSchemaValue(InvalidSchemaValue): + original_exception = attr.ib() + + def __str__(self): + return self.msg.format(value=self.value, type=self.type, exception=self.original_exception) +@attr.s class UndefinedSchemaProperty(OpenAPISchemaError): - pass + extra_props = attr.ib() + + def __str__(self): + return "Extra unexpected properties found in schema: {0}".format(self.extra_props) +@attr.s class MissingSchemaProperty(OpenAPISchemaError): - pass + property_name = attr.ib() + + def __str__(self): + return "Missing schema property: {0}".format(self.property_name) +@attr.s class NoOneOfSchema(OpenAPISchemaError): - pass + type = attr.ib() + + def __str__(self): + return "Exactly one valid schema type {0} should be valid, None found.".format(self.type) +@attr.s class MultipleOneOfSchema(OpenAPISchemaError): - pass + type = attr.ib() + + def __str__(self): + return "Exactly one schema type {0} should be valid, more than one found".format(self.type) diff --git a/openapi_core/schema/schemas/models.py b/openapi_core/schema/schemas/models.py index d5ff5b7..9702049 100644 --- a/openapi_core/schema/schemas/models.py +++ b/openapi_core/schema/schemas/models.py @@ -14,7 +14,7 @@ from openapi_core.schema.schemas.enums import SchemaFormat, SchemaType from openapi_core.schema.schemas.exceptions import ( InvalidSchemaValue, UndefinedSchemaProperty, MissingSchemaProperty, OpenAPISchemaError, NoOneOfSchema, MultipleOneOfSchema, NoValidSchema, - UndefinedItemsSchema, + UndefinedItemsSchema, InvalidCustomFormatSchemaValue, ) from openapi_core.schema.schemas.util import ( forcebool, format_date, format_datetime, @@ -160,7 +160,7 @@ class Schema(object): """Cast value to schema type""" if value is None: if not self.nullable: - raise InvalidSchemaValue("Null value for non-nullable schema") + raise InvalidSchemaValue("Null value for non-nullable schema", value, self.type) return self.default cast_mapping = self.get_cast_mapping(custom_formatters=custom_formatters) @@ -173,8 +173,7 @@ class Schema(object): return cast_callable(value) except ValueError: raise InvalidSchemaValue( - "Failed to cast value of {0} to {1}".format(value, self.type) - ) + "Failed to cast value {value} to type {type}", value, self.type) def unmarshal(self, value, custom_formatters=None): """Unmarshal parameter from the value.""" @@ -188,9 +187,7 @@ class Schema(object): if self.enum and casted not in self.enum: raise InvalidSchemaValue( - "Value of {0} not in enum choices: {1}".format( - value, self.enum) - ) + "Value {value} not in enum choices: {type}", value, self.enum) return casted @@ -198,23 +195,21 @@ class Schema(object): try: schema_format = SchemaFormat(self.format) except ValueError: - msg = "Unsupported {0} format unmarshalling".format(self.format) + 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 OpenAPISchemaError(msg) + raise InvalidSchemaValue(msg, value, self.format) else: - raise OpenAPISchemaError(msg) + raise InvalidSchemaValue(msg, value, self.format) else: formatstring = self.STRING_FORMAT_CALLABLE_GETTER[schema_format] try: return formatstring.unmarshal(value) - except ValueError: - raise InvalidSchemaValue( - "Failed to format value of {0} to {1}".format( - value, self.format) - ) + except ValueError as exc: + raise InvalidCustomFormatSchemaValue( + "Failed to format value {value} to format {type}: {exception}", value, self.format, exc) def _unmarshal_any(self, value, custom_formatters=None): types_resolve_order = [ @@ -230,12 +225,11 @@ class Schema(object): except (OpenAPISchemaError, TypeError, ValueError): continue - raise NoValidSchema( - "No valid schema found for value {0}".format(value)) + raise NoValidSchema(value) def _unmarshal_collection(self, value, custom_formatters=None): if self.items is None: - raise UndefinedItemsSchema("Undefined items' schema") + raise UndefinedItemsSchema(self.type) f = functools.partial(self.items.unmarshal, custom_formatters=custom_formatters) @@ -244,8 +238,7 @@ class Schema(object): def _unmarshal_object(self, value, model_factory=None, custom_formatters=None): if not isinstance(value, (dict, )): - raise InvalidSchemaValue( - "Value of {0} not a dict".format(value)) + raise InvalidSchemaValue("Value {value} is not of type {type}", value, self.type) model_factory = model_factory or ModelFactory() @@ -259,14 +252,11 @@ class Schema(object): pass else: if properties is not None: - raise MultipleOneOfSchema( - "Exactly one schema should be valid," - "multiple found") + raise MultipleOneOfSchema(self.type) properties = found_props if properties is None: - raise NoOneOfSchema( - "Exactly one valid schema should be valid, None found.") + raise NoOneOfSchema(self.type) else: properties = self._unmarshal_properties( @@ -290,8 +280,7 @@ class Schema(object): value_props_names = value.keys() extra_props = set(value_props_names) - set(all_props_names) if extra_props and self.additional_properties is None: - raise UndefinedSchemaProperty( - "Undefined properties in schema: {0}".format(extra_props)) + raise UndefinedSchemaProperty(extra_props) properties = {} for prop_name in extra_props: @@ -304,8 +293,7 @@ class Schema(object): prop_value = value[prop_name] except KeyError: if prop_name in all_req_props_names: - raise MissingSchemaProperty( - "Missing schema property {0}".format(prop_name)) + raise MissingSchemaProperty(prop_name) if not prop.nullable and not prop.default: continue prop_value = prop.default @@ -334,7 +322,7 @@ class Schema(object): def validate(self, value, custom_formatters=None): if value is None: if not self.nullable: - raise InvalidSchemaValue("Null value for non-nullable schema") + raise InvalidSchemaValue("Null value for non-nullable schema of type {type}", value, self.type) return # type validation @@ -342,9 +330,7 @@ class Schema(object): self.type] if not type_validator_callable(value): raise InvalidSchemaValue( - "Value of {0} not valid type of {1}".format( - value, self.type.value) - ) + "Value {value} not valid type {type}", value, self.type.value) # structure validation validator_mapping = self.get_validator_mapping() @@ -355,7 +341,7 @@ class Schema(object): def _validate_collection(self, value, custom_formatters=None): if self.items is None: - raise OpenAPISchemaError("Schema for collection not defined") + raise UndefinedItemsSchema(self.type) if self.min_items is not None: if self.min_items < 0: @@ -436,9 +422,7 @@ class Schema(object): if not formatstring.validate(value): raise InvalidSchemaValue( - "Value of {0} not valid format of {1}".format( - value, self.format) - ) + "Value {value} not valid format {type}", value, self.format) if self.min_length is not None: if self.min_length < 0: @@ -484,14 +468,11 @@ class Schema(object): pass else: if valid_one_of_schema is not None: - raise MultipleOneOfSchema( - "Exactly one schema should be valid," - "multiple found") + raise MultipleOneOfSchema(self.type) valid_one_of_schema = True if valid_one_of_schema is None: - raise NoOneOfSchema( - "Exactly one valid schema should be valid, None found.") + raise NoOneOfSchema(self.type) else: self._validate_properties(properties, @@ -542,8 +523,7 @@ class Schema(object): value_props_names = value.keys() extra_props = set(value_props_names) - set(all_props_names) if extra_props and self.additional_properties is None: - raise UndefinedSchemaProperty( - "Undefined properties in schema: {0}".format(extra_props)) + raise UndefinedSchemaProperty(extra_props) for prop_name in extra_props: prop_value = value[prop_name] @@ -555,8 +535,7 @@ class Schema(object): prop_value = value[prop_name] except KeyError: if prop_name in all_req_props_names: - raise MissingSchemaProperty( - "Missing schema property {0}".format(prop_name)) + raise MissingSchemaProperty(prop_name) if not prop.nullable and not prop.default: continue prop_value = prop.default diff --git a/openapi_core/schema/servers/exceptions.py b/openapi_core/schema/servers/exceptions.py index fbce778..1ae0d3a 100644 --- a/openapi_core/schema/servers/exceptions.py +++ b/openapi_core/schema/servers/exceptions.py @@ -1,9 +1,16 @@ from openapi_core.schema.exceptions import OpenAPIMappingError +import attr + class OpenAPIServerError(OpenAPIMappingError): pass +@attr.s class InvalidServer(OpenAPIServerError): - pass + full_url_pattern = attr.ib() + + def __str__(self): + return "Invalid request server {0}".format( + self.full_url_pattern) diff --git a/openapi_core/schema/specs/models.py b/openapi_core/schema/specs/models.py index 629c24e..e1aa893 100644 --- a/openapi_core/schema/specs/models.py +++ b/openapi_core/schema/specs/models.py @@ -31,8 +31,7 @@ class Spec(object): if spec_server.default_url in full_url_pattern: return spec_server - raise InvalidServer( - "Invalid request server {0}".format(full_url_pattern)) + raise InvalidServer(full_url_pattern) def get_server_url(self, index=0): return self.servers[index].default_url @@ -41,9 +40,7 @@ class Spec(object): try: return self.paths[path_pattern].operations[http_method] except KeyError: - raise InvalidOperation( - "Unknown operation path {0} with method {1}".format( - path_pattern, http_method)) + raise InvalidOperation(path_pattern, http_method) def get_schema(self, name): return self.components.schemas[name] From b66ec04ac2fcae4a67b3eaec022730e27850d65b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Domen=20Ko=C5=BEar?= Date: Wed, 12 Sep 2018 17:43:07 +0100 Subject: [PATCH 4/7] parameters/body should catch all schema errors --- openapi_core/schema/media_types/models.py | 6 +++--- openapi_core/schema/parameters/models.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openapi_core/schema/media_types/models.py b/openapi_core/schema/media_types/models.py index 8635bdf..5104095 100644 --- a/openapi_core/schema/media_types/models.py +++ b/openapi_core/schema/media_types/models.py @@ -4,7 +4,7 @@ from collections import defaultdict from json import loads from openapi_core.schema.media_types.exceptions import InvalidMediaTypeValue -from openapi_core.schema.schemas.exceptions import InvalidSchemaValue +from openapi_core.schema.schemas.exceptions import OpenAPISchemaError MEDIA_TYPE_DESERIALIZERS = { @@ -43,10 +43,10 @@ class MediaType(object): try: unmarshalled = self.schema.unmarshal(deserialized, custom_formatters=custom_formatters) - except InvalidSchemaValue as exc: + except OpenAPISchemaError as exc: raise InvalidMediaTypeValue(exc) try: return self.schema.validate(unmarshalled, custom_formatters=custom_formatters) - except InvalidSchemaValue as exc: + except OpenAPISchemaError as exc: raise InvalidMediaTypeValue(exc) diff --git a/openapi_core/schema/parameters/models.py b/openapi_core/schema/parameters/models.py index dcc8fd7..f33773f 100644 --- a/openapi_core/schema/parameters/models.py +++ b/openapi_core/schema/parameters/models.py @@ -10,7 +10,7 @@ from openapi_core.schema.parameters.exceptions import ( EmptyParameterValue, ) from openapi_core.schema.schemas.enums import SchemaType -from openapi_core.schema.schemas.exceptions import InvalidSchemaValue +from openapi_core.schema.schemas.exceptions import OpenAPISchemaError log = logging.getLogger(__name__) @@ -110,10 +110,10 @@ class Parameter(object): try: unmarshalled = self.schema.unmarshal(deserialized, custom_formatters=custom_formatters) - except InvalidSchemaValue as exc: + except OpenAPISchemaError as exc: raise InvalidParameterValue(self.name, exc) try: return self.schema.validate(unmarshalled, custom_formatters=custom_formatters) - except InvalidSchemaValue as exc: + except OpenAPISchemaError as exc: raise InvalidParameterValue(self.name, exc) From 17ea740b7b20c683be03fe5cc71f40e4eb043ac9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Domen=20Ko=C5=BEar?= Date: Wed, 12 Sep 2018 17:43:31 +0100 Subject: [PATCH 5/7] Add field name to all property errors --- openapi_core/schema/schemas/exceptions.py | 7 +++++++ openapi_core/schema/schemas/models.py | 14 ++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/openapi_core/schema/schemas/exceptions.py b/openapi_core/schema/schemas/exceptions.py index d8d38e4..b4656aa 100644 --- a/openapi_core/schema/schemas/exceptions.py +++ b/openapi_core/schema/schemas/exceptions.py @@ -47,6 +47,13 @@ class UndefinedSchemaProperty(OpenAPISchemaError): def __str__(self): return "Extra unexpected properties found in schema: {0}".format(self.extra_props) +@attr.s +class InvalidSchemaProperty(OpenAPISchemaError): + property_name = attr.ib() + original_exception = attr.ib() + + def __str__(self): + return "Invalid schema property {0}: {1}".format(self.property_name, self.original_exception) @attr.s class MissingSchemaProperty(OpenAPISchemaError): diff --git a/openapi_core/schema/schemas/models.py b/openapi_core/schema/schemas/models.py index 9702049..6434cd5 100644 --- a/openapi_core/schema/schemas/models.py +++ b/openapi_core/schema/schemas/models.py @@ -14,7 +14,7 @@ from openapi_core.schema.schemas.enums import SchemaFormat, SchemaType from openapi_core.schema.schemas.exceptions import ( InvalidSchemaValue, UndefinedSchemaProperty, MissingSchemaProperty, OpenAPISchemaError, NoOneOfSchema, MultipleOneOfSchema, NoValidSchema, - UndefinedItemsSchema, InvalidCustomFormatSchemaValue, + UndefinedItemsSchema, InvalidCustomFormatSchemaValue, InvalidSchemaProperty, ) from openapi_core.schema.schemas.util import ( forcebool, format_date, format_datetime, @@ -297,8 +297,11 @@ class Schema(object): if not prop.nullable and not prop.default: continue prop_value = prop.default - properties[prop_name] = prop.unmarshal( - prop_value, custom_formatters=custom_formatters) + try: + properties[prop_name] = prop.unmarshal( + prop_value, custom_formatters=custom_formatters) + except OpenAPISchemaError as exc: + raise InvalidSchemaProperty(prop_name, exc) self._validate_properties(properties, one_of_schema=one_of_schema, custom_formatters=custom_formatters) @@ -539,6 +542,9 @@ class Schema(object): if not prop.nullable and not prop.default: continue prop_value = prop.default - prop.validate(prop_value, custom_formatters=custom_formatters) + try: + prop.validate(prop_value, custom_formatters=custom_formatters) + except OpenAPISchemaError as exc: + raise InvalidSchemaProperty(prop_name, original_exception=exc) return True From 399602f013b32fb956ad2895ce7e9d36d2885159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Domen=20Ko=C5=BEar?= Date: Thu, 13 Sep 2018 11:59:44 +0100 Subject: [PATCH 6/7] sync master changes with exception refactoring --- openapi_core/schema/schemas/models.py | 57 ++++++++++----------------- tests/integration/test_petstore.py | 7 ++-- 2 files changed, 24 insertions(+), 40 deletions(-) diff --git a/openapi_core/schema/schemas/models.py b/openapi_core/schema/schemas/models.py index 6434cd5..50783f7 100644 --- a/openapi_core/schema/schemas/models.py +++ b/openapi_core/schema/schemas/models.py @@ -354,10 +354,8 @@ class Schema(object): ) if len(value) < self.min_items: raise InvalidSchemaValue( - "Value must contain at least {0} item(s)," - " {1} found".format( - self.min_items, len(value)) - ) + "Value must contain at least {type} item(s)," + " {value} found", len(value), self.min_items) if self.max_items is not None: if self.max_items < 0: raise OpenAPISchemaError( @@ -366,47 +364,36 @@ class Schema(object): ) if len(value) > self.max_items: raise InvalidSchemaValue( - "Value must contain at most {0} item(s)," - " {1} found".format( - self.max_items, len(value)) - ) + "Value must contain at most {value} item(s)," + " {type} found", len(value), self.max_items) if self.unique_items and len(set(value)) != len(value): - raise InvalidSchemaValue("Value may not contain duplicate items") + raise OpenAPISchemaError("Value may not contain duplicate items") f = functools.partial(self.items.validate, custom_formatters=custom_formatters) return list(map(f, value)) - def _validate_number(self, value): + def _validate_number(self, value, custom_formatters=None): if self.minimum is not None: if self.exclusive_minimum and value <= self.minimum: raise InvalidSchemaValue( - "Value {0} is not less than or equal to {1}".format( - value, self.minimum) - ) + "Value {value} is not less than or equal to {type}", value, self.minimum) elif value < self.minimum: raise InvalidSchemaValue( - "Value {0} is not less than {1}".format( - value, self.minimum) - ) + "Value {value} is not less than {type}", value, self.minimum) if self.maximum is not None: if self.exclusive_maximum and value >= self.maximum: raise InvalidSchemaValue( - "Value {0} is not greater than or equal to {1}".format( - value, self.maximum) - ) + "Value {value} is not greater than or equal to {type}", value, self.maximum) elif value > self.maximum: raise InvalidSchemaValue( - "Value {0} is not greater than {1}".format( - value, self.maximum) - ) + "Value {value} is not greater than {type}", value, self.maximum) if self.multiple_of is not None and value % self.multiple_of: raise InvalidSchemaValue( - "Value {0} is not a multiple of {1}".format( + "Value {value} is not a multiple of {type}", value, self.multiple_of) - ) def _validate_string(self, value, custom_formatters=None): try: @@ -435,8 +422,8 @@ class Schema(object): ) if len(value) < self.min_length: raise InvalidSchemaValue( - "Value is shorter than the minimum length of {0}".format( - self.min_length) + "Value is shorter ({value}) than the minimum length of {type}", + len(value), self.min_length ) if self.max_length is not None: if self.max_length < 0: @@ -446,13 +433,13 @@ class Schema(object): ) if len(value) > self.max_length: raise InvalidSchemaValue( - "Value is longer than the maximum length of {0}".format( - self.max_length) + "Value is longer ({value}) than the maximum length of {type}", + len(value), self.max_length ) if self.pattern is not None and not self.pattern.search(value): raise InvalidSchemaValue( - "Value {0} does not match the pattern {1}".format( - value, self.pattern.pattern) + "Value {value} does not match the pattern {type}", + value, self.pattern.pattern ) return True @@ -490,9 +477,8 @@ class Schema(object): if len(properties) < self.min_properties: raise InvalidSchemaValue( - "Value must contain at least {0} properties," - " {1} found".format( - self.min_properties, len(properties)) + "Value must contain at least {type} properties," + " {value} found", len(properties), self.min_properties ) if self.max_properties is not None: @@ -503,9 +489,8 @@ class Schema(object): ) if len(properties) > self.max_properties: raise InvalidSchemaValue( - "Value must contain at most {0} properties," - " {1} found".format( - self.max_properties, len(properties)) + "Value must contain at most {type} properties," + " {value} found", len(properties), self.max_properties ) return True diff --git a/tests/integration/test_petstore.py b/tests/integration/test_petstore.py index 80e0cb2..d7fd1a0 100644 --- a/tests/integration/test_petstore.py +++ b/tests/integration/test_petstore.py @@ -16,7 +16,6 @@ from openapi_core.schema.paths.models import Path from openapi_core.schema.request_bodies.models import RequestBody from openapi_core.schema.responses.models import Response from openapi_core.schema.schemas.exceptions import ( - UndefinedSchemaProperty, MissingSchemaProperty, NoOneOfSchema, NoValidSchema, ) from openapi_core.schema.schemas.models import Schema @@ -615,7 +614,7 @@ class TestPetstore(object): }, } - with pytest.raises(NoOneOfSchema): + with pytest.raises(InvalidMediaTypeValue): request.get_body(spec) def test_post_cats_only_required_body(self, spec, spec_dict): @@ -952,7 +951,7 @@ class TestPetstore(object): assert parameters == {} - with pytest.raises(UndefinedSchemaProperty): + with pytest.raises(InvalidMediaTypeValue): request.get_body(spec) def test_post_tags_empty_body(self, spec, spec_dict): @@ -970,7 +969,7 @@ class TestPetstore(object): assert parameters == {} - with pytest.raises(MissingSchemaProperty): + with pytest.raises(InvalidMediaTypeValue): request.get_body(spec) def test_post_tags_wrong_property_type(self, spec): From e1c64792d2082b003e0c8cece06e02829b02045e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Domen=20Ko=C5=BEar?= Date: Thu, 13 Sep 2018 13:57:59 +0100 Subject: [PATCH 7/7] typo --- openapi_core/schema/parameters/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi_core/schema/parameters/exceptions.py b/openapi_core/schema/parameters/exceptions.py index c159301..853d067 100644 --- a/openapi_core/schema/parameters/exceptions.py +++ b/openapi_core/schema/parameters/exceptions.py @@ -37,4 +37,4 @@ class InvalidParameterValue(OpenAPIParameterError): original_exception = attr.ib() def __str__(self): - return "Invalid parameter value for `{0}`: {1}".format(sef.name, self.original_exception) + return "Invalid parameter value for `{0}`: {1}".format(self.name, self.original_exception)