Merge pull request #94 from domenkozar/structured-exceptions

Structured exceptions
This commit is contained in:
A 2018-09-13 20:41:33 +01:00 committed by GitHub
commit 2e6f2dedc9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 305 additions and 204 deletions

View file

@ -1,9 +1,16 @@
from openapi_core.schema.exceptions import OpenAPIMappingError from openapi_core.schema.exceptions import OpenAPIMappingError
import attr
class OpenAPIContentError(OpenAPIMappingError): class OpenAPIContentError(OpenAPIMappingError):
pass pass
@attr.s
class MimeTypeNotFound(OpenAPIContentError): 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)

View file

@ -18,4 +18,4 @@ class Content(dict):
if fnmatch.fnmatch(mimetype, key): if fnmatch.fnmatch(mimetype, key):
return value return value
raise MimeTypeNotFound("{0} mimetype not found") raise MimeTypeNotFound(mimetype, self.keys())

View file

@ -1,13 +1,21 @@
from openapi_core.schema.exceptions import OpenAPIMappingError from openapi_core.schema.exceptions import OpenAPIMappingError
import attr
class OpenAPIMediaTypeError(OpenAPIMappingError): class OpenAPIMediaTypeError(OpenAPIMappingError):
pass pass
@attr.s
class InvalidMediaTypeValue(OpenAPIMediaTypeError): class InvalidMediaTypeValue(OpenAPIMediaTypeError):
pass original_exception = attr.ib()
def __str__(self):
return "Mimetype invalid: {0}".format(self.original_exception)
@attr.s
class InvalidContentType(OpenAPIMediaTypeError): class InvalidContentType(OpenAPIMediaTypeError):
pass mimetype = attr.ib()
def __str__(self):
return "Content for following mimetype not found: {0}".format(self.mimetype)

View file

@ -4,7 +4,7 @@ from collections import defaultdict
from json import loads from json import loads
from openapi_core.schema.media_types.exceptions import InvalidMediaTypeValue 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 = { MEDIA_TYPE_DESERIALIZERS = {
@ -32,21 +32,21 @@ class MediaType(object):
deserializer = self.get_dererializer() deserializer = self.get_dererializer()
return deserializer(value) return deserializer(value)
def unmarshal(self, value): def unmarshal(self, value, custom_formatters=None):
if not self.schema: if not self.schema:
return value return value
try: try:
deserialized = self.deserialize(value) deserialized = self.deserialize(value)
except ValueError as exc: except ValueError as exc:
raise InvalidMediaTypeValue(str(exc)) raise InvalidMediaTypeValue(exc)
try: try:
unmarshalled = self.schema.unmarshal(deserialized) unmarshalled = self.schema.unmarshal(deserialized, custom_formatters=custom_formatters)
except InvalidSchemaValue as exc: except OpenAPISchemaError as exc:
raise InvalidMediaTypeValue(str(exc)) raise InvalidMediaTypeValue(exc)
try: try:
return self.schema.validate(unmarshalled) return self.schema.validate(unmarshalled, custom_formatters=custom_formatters)
except InvalidSchemaValue as exc: except OpenAPISchemaError as exc:
raise InvalidMediaTypeValue(str(exc)) raise InvalidMediaTypeValue(exc)

View file

@ -1,9 +1,17 @@
from openapi_core.schema.exceptions import OpenAPIMappingError from openapi_core.schema.exceptions import OpenAPIMappingError
import attr
class OpenAPIOperationError(OpenAPIMappingError): class OpenAPIOperationError(OpenAPIMappingError):
pass pass
@attr.s
class InvalidOperation(OpenAPIOperationError): 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)

View file

@ -32,7 +32,6 @@ class Operation(object):
return self.responses[http_status_range] return self.responses[http_status_range]
if 'default' not in self.responses: if 'default' not in self.responses:
raise InvalidResponse( raise InvalidResponse(http_status, self.responses)
"Unknown response http status {0}".format(http_status))
return self.responses['default'] return self.responses['default']

View file

@ -1,21 +1,40 @@
from openapi_core.schema.exceptions import OpenAPIMappingError from openapi_core.schema.exceptions import OpenAPIMappingError
import attr
class OpenAPIParameterError(OpenAPIMappingError): class OpenAPIParameterError(OpenAPIMappingError):
pass pass
@attr.s
class MissingParameter(OpenAPIParameterError): 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): class MissingRequiredParameter(OpenAPIParameterError):
pass name = attr.ib()
def __str__(self):
return "Missing required parameter: {0}".format(self.name)
@attr.s
class EmptyParameterValue(OpenAPIParameterError): 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): class InvalidParameterValue(OpenAPIParameterError):
pass name = attr.ib()
original_exception = attr.ib()
def __str__(self):
return "Invalid parameter value for `{0}`: {1}".format(self.name, self.original_exception)

View file

@ -10,7 +10,7 @@ from openapi_core.schema.parameters.exceptions import (
EmptyParameterValue, EmptyParameterValue,
) )
from openapi_core.schema.schemas.enums import SchemaType 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__) log = logging.getLogger(__name__)
@ -77,12 +77,10 @@ class Parameter(object):
if self.name not in location: if self.name not in location:
if self.required: if self.required:
raise MissingRequiredParameter( raise MissingRequiredParameter(self.name)
"Missing required `{0}` parameter".format(self.name))
if not self.schema or self.schema.default is None: if not self.schema or self.schema.default is None:
raise MissingParameter( raise MissingParameter(self.name)
"Missing `{0}` parameter".format(self.name))
return self.schema.default return self.schema.default
@ -91,7 +89,7 @@ class Parameter(object):
return location[self.name] return location[self.name]
def unmarshal(self, value): def unmarshal(self, value, custom_formatters=None):
if self.deprecated: if self.deprecated:
warnings.warn( warnings.warn(
"{0} parameter is deprecated".format(self.name), "{0} parameter is deprecated".format(self.name),
@ -100,8 +98,7 @@ class Parameter(object):
if (self.location == ParameterLocation.QUERY and value == "" and if (self.location == ParameterLocation.QUERY and value == "" and
not self.allow_empty_value): not self.allow_empty_value):
raise EmptyParameterValue( raise EmptyParameterValue(self.name)
"Value of {0} parameter cannot be empty".format(self.name))
if not self.schema: if not self.schema:
return value return value
@ -109,14 +106,14 @@ class Parameter(object):
try: try:
deserialized = self.deserialize(value) deserialized = self.deserialize(value)
except (ValueError, AttributeError) as exc: except (ValueError, AttributeError) as exc:
raise InvalidParameterValue(str(exc)) raise InvalidParameterValue(self.name, exc)
try: try:
unmarshalled = self.schema.unmarshal(deserialized) unmarshalled = self.schema.unmarshal(deserialized, custom_formatters=custom_formatters)
except InvalidSchemaValue as exc: except OpenAPISchemaError as exc:
raise InvalidParameterValue(str(exc)) raise InvalidParameterValue(self.name, exc)
try: try:
return self.schema.validate(unmarshalled) return self.schema.validate(unmarshalled, custom_formatters=custom_formatters)
except InvalidSchemaValue as exc: except OpenAPISchemaError as exc:
raise InvalidParameterValue(str(exc)) raise InvalidParameterValue(self.name, exc)

View file

@ -1,9 +1,15 @@
from openapi_core.schema.exceptions import OpenAPIMappingError from openapi_core.schema.exceptions import OpenAPIMappingError
import attr
class OpenAPIRequestBodyError(OpenAPIMappingError): class OpenAPIRequestBodyError(OpenAPIMappingError):
pass pass
@attr.s
class MissingRequestBody(OpenAPIRequestBodyError): class MissingRequestBody(OpenAPIRequestBodyError):
pass request = attr.ib()
def __str__(self):
return "Missing required request body"

View file

@ -16,11 +16,9 @@ class RequestBody(object):
try: try:
return self.content[mimetype] return self.content[mimetype]
except MimeTypeNotFound: except MimeTypeNotFound:
raise InvalidContentType( raise InvalidContentType(mimetype)
"Invalid mime type `{0}`".format(mimetype))
def get_value(self, request): def get_value(self, request):
if not request.body and self.required: if not request.body and self.required:
raise MissingRequestBody("Missing required request body") raise MissingRequestBody(request)
return request.body return request.body

View file

@ -1,13 +1,24 @@
from openapi_core.schema.exceptions import OpenAPIMappingError from openapi_core.schema.exceptions import OpenAPIMappingError
import attr
class OpenAPIResponseError(OpenAPIMappingError): class OpenAPIResponseError(OpenAPIMappingError):
pass pass
@attr.s
class InvalidResponse(OpenAPIResponseError): 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): class MissingResponseContent(OpenAPIResponseError):
pass response = attr.ib()
def __str__(self):
return "Missing response content"

View file

@ -23,11 +23,10 @@ class Response(object):
try: try:
return self.content[mimetype] return self.content[mimetype]
except MimeTypeNotFound: except MimeTypeNotFound:
raise InvalidContentType( raise InvalidContentType(mimetype)
"Invalid mime type `{0}`".format(mimetype))
def get_value(self, response): def get_value(self, response):
if not response.data: if not response.data:
raise MissingResponseContent("Missing response content") raise MissingResponseContent(response)
return response.data return response.data

View file

@ -1,33 +1,79 @@
from openapi_core.schema.exceptions import OpenAPIMappingError from openapi_core.schema.exceptions import OpenAPIMappingError
import attr
class OpenAPISchemaError(OpenAPIMappingError): class OpenAPISchemaError(OpenAPIMappingError):
pass pass
@attr.s
class NoValidSchema(OpenAPISchemaError): 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): 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): 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): 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 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): 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): 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): 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)

View file

@ -1,4 +1,6 @@
"""OpenAPI core schemas models module""" """OpenAPI core schemas models module"""
import attr
import functools
import logging import logging
from collections import defaultdict from collections import defaultdict
from datetime import date, datetime from datetime import date, datetime
@ -12,7 +14,7 @@ from openapi_core.schema.schemas.enums import SchemaFormat, SchemaType
from openapi_core.schema.schemas.exceptions import ( from openapi_core.schema.schemas.exceptions import (
InvalidSchemaValue, UndefinedSchemaProperty, MissingSchemaProperty, InvalidSchemaValue, UndefinedSchemaProperty, MissingSchemaProperty,
OpenAPISchemaError, NoOneOfSchema, MultipleOneOfSchema, NoValidSchema, OpenAPISchemaError, NoOneOfSchema, MultipleOneOfSchema, NoValidSchema,
UndefinedItemsSchema, UndefinedItemsSchema, InvalidCustomFormatSchemaValue, InvalidSchemaProperty,
) )
from openapi_core.schema.schemas.util import ( from openapi_core.schema.schemas.util import (
forcebool, format_date, format_datetime, forcebool, format_date, format_datetime,
@ -24,6 +26,12 @@ from openapi_core.schema.schemas.validators import (
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@attr.s
class Format(object):
unmarshal = attr.ib()
validate = attr.ib()
class Schema(object): class Schema(object):
"""Represents an OpenAPI Schema.""" """Represents an OpenAPI Schema."""
@ -33,18 +41,11 @@ class Schema(object):
SchemaType.BOOLEAN: forcebool, SchemaType.BOOLEAN: forcebool,
} }
STRING_FORMAT_CAST_CALLABLE_GETTER = { STRING_FORMAT_CALLABLE_GETTER = {
SchemaFormat.NONE: text_type, SchemaFormat.NONE: Format(text_type, TypeValidator(text_type)),
SchemaFormat.DATE: format_date, SchemaFormat.DATE: Format(format_date, TypeValidator(date, exclude=datetime)),
SchemaFormat.DATETIME: format_datetime, SchemaFormat.DATETIME: Format(format_datetime, TypeValidator(datetime)),
SchemaFormat.BINARY: binary_type, SchemaFormat.BINARY: Format(binary_type, TypeValidator(binary_type)),
}
STRING_FORMAT_VALIDATOR_CALLABLE_GETTER = {
SchemaFormat.NONE: TypeValidator(text_type),
SchemaFormat.DATE: TypeValidator(date, exclude=datetime),
SchemaFormat.DATETIME: TypeValidator(datetime),
SchemaFormat.BINARY: TypeValidator(binary_type),
} }
TYPE_VALIDATOR_CALLABLE_GETTER = { TYPE_VALIDATOR_CALLABLE_GETTER = {
@ -142,25 +143,27 @@ class Schema(object):
return set(required) 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 = self.DEFAULT_CAST_CALLABLE_GETTER.copy()
mapping.update({ mapping.update({
SchemaType.STRING: self._unmarshal_string, SchemaType.STRING: pass_defaults(self._unmarshal_string),
SchemaType.ANY: self._unmarshal_any, SchemaType.ANY: pass_defaults(self._unmarshal_any),
SchemaType.ARRAY: self._unmarshal_collection, SchemaType.ARRAY: pass_defaults(self._unmarshal_collection),
SchemaType.OBJECT: self._unmarshal_object, SchemaType.OBJECT: pass_defaults(self._unmarshal_object),
}) })
return defaultdict(lambda: lambda x: x, mapping) return defaultdict(lambda: lambda x: x, mapping)
def cast(self, value): def cast(self, value, custom_formatters=None):
"""Cast value to schema type""" """Cast value to schema type"""
if value is None: if value is None:
if not self.nullable: 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 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 == '': if self.type is not SchemaType.STRING and value == '':
return None return None
@ -170,47 +173,45 @@ class Schema(object):
return cast_callable(value) return cast_callable(value)
except ValueError: except ValueError:
raise InvalidSchemaValue( 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): def unmarshal(self, value, custom_formatters=None):
"""Unmarshal parameter from the value.""" """Unmarshal parameter from the value."""
if self.deprecated: if self.deprecated:
warnings.warn("The schema is deprecated", DeprecationWarning) warnings.warn("The schema is deprecated", DeprecationWarning)
casted = self.cast(value) casted = self.cast(value, custom_formatters=custom_formatters)
if casted is None and not self.required: if casted is None and not self.required:
return None return None
if self.enum and casted not in self.enum: if self.enum and casted not in self.enum:
raise InvalidSchemaValue( raise InvalidSchemaValue(
"Value of {0} not in enum choices: {1}".format( "Value {value} not in enum choices: {type}", value, self.enum)
value, self.enum)
)
return casted return casted
def _unmarshal_string(self, value): def _unmarshal_string(self, value, custom_formatters=None):
try: try:
schema_format = SchemaFormat(self.format) schema_format = SchemaFormat(self.format)
except ValueError: except ValueError:
# @todo: implement custom format unmarshalling support msg = "Unsupported format {type} unmarshalling for value {value}"
raise OpenAPISchemaError( if custom_formatters is not None:
"Unsupported {0} format unmarshalling".format(self.format) formatstring = custom_formatters.get(self.format)
) if formatstring is None:
raise InvalidSchemaValue(msg, value, self.format)
else:
raise InvalidSchemaValue(msg, value, self.format)
else: else:
formatter = self.STRING_FORMAT_CAST_CALLABLE_GETTER[schema_format] formatstring = self.STRING_FORMAT_CALLABLE_GETTER[schema_format]
try: try:
return formatter(value) return formatstring.unmarshal(value)
except ValueError: except ValueError as exc:
raise InvalidSchemaValue( raise InvalidCustomFormatSchemaValue(
"Failed to format value of {0} to {1}".format( "Failed to format value {value} to format {type}: {exception}", value, self.format, exc)
value, self.format)
)
def _unmarshal_any(self, value): def _unmarshal_any(self, value, custom_formatters=None):
types_resolve_order = [ types_resolve_order = [
SchemaType.OBJECT, SchemaType.ARRAY, SchemaType.BOOLEAN, SchemaType.OBJECT, SchemaType.ARRAY, SchemaType.BOOLEAN,
SchemaType.INTEGER, SchemaType.NUMBER, SchemaType.STRING, SchemaType.INTEGER, SchemaType.NUMBER, SchemaType.STRING,
@ -224,19 +225,20 @@ class Schema(object):
except (OpenAPISchemaError, TypeError, ValueError): except (OpenAPISchemaError, TypeError, ValueError):
continue continue
raise NoValidSchema( raise NoValidSchema(value)
"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: if self.items is None:
raise UndefinedItemsSchema("Undefined items' schema") raise UndefinedItemsSchema(self.type)
return list(map(self.items.unmarshal, value)) 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, )): if not isinstance(value, (dict, )):
raise InvalidSchemaValue( raise InvalidSchemaValue("Value {value} is not of type {type}", value, self.type)
"Value of {0} not a dict".format(value))
model_factory = model_factory or ModelFactory() model_factory = model_factory or ModelFactory()
@ -245,26 +247,25 @@ class Schema(object):
for one_of_schema in self.one_of: for one_of_schema in self.one_of:
try: try:
found_props = self._unmarshal_properties( found_props = self._unmarshal_properties(
value, one_of_schema) value, one_of_schema, custom_formatters=custom_formatters)
except OpenAPISchemaError: except OpenAPISchemaError:
pass pass
else: else:
if properties is not None: if properties is not None:
raise MultipleOneOfSchema( raise MultipleOneOfSchema(self.type)
"Exactly one schema should be valid,"
"multiple found")
properties = found_props properties = found_props
if properties is None: if properties is None:
raise NoOneOfSchema( raise NoOneOfSchema(self.type)
"Exactly one valid schema should be valid, None found.")
else: else:
properties = self._unmarshal_properties(value) properties = self._unmarshal_properties(
value, custom_formatters=custom_formatters)
return model_factory.create(properties, name=self.model) 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 = self.get_all_properties()
all_props_names = self.get_all_properties_names() all_props_names = self.get_all_properties_names()
all_req_props_names = self.get_all_required_properties_names() all_req_props_names = self.get_all_required_properties_names()
@ -279,28 +280,31 @@ class Schema(object):
value_props_names = value.keys() value_props_names = value.keys()
extra_props = set(value_props_names) - set(all_props_names) extra_props = set(value_props_names) - set(all_props_names)
if extra_props and self.additional_properties is None: if extra_props and self.additional_properties is None:
raise UndefinedSchemaProperty( raise UndefinedSchemaProperty(extra_props)
"Undefined properties in schema: {0}".format(extra_props))
properties = {} properties = {}
for prop_name in extra_props: for prop_name in extra_props:
prop_value = value[prop_name] prop_value = value[prop_name]
properties[prop_name] = self.additional_properties.unmarshal( properties[prop_name] = self.additional_properties.unmarshal(
prop_value) prop_value, custom_formatters=custom_formatters)
for prop_name, prop in iteritems(all_props): for prop_name, prop in iteritems(all_props):
try: try:
prop_value = value[prop_name] prop_value = value[prop_name]
except KeyError: except KeyError:
if prop_name in all_req_props_names: if prop_name in all_req_props_names:
raise MissingSchemaProperty( raise MissingSchemaProperty(prop_name)
"Missing schema property {0}".format(prop_name))
if not prop.nullable and not prop.default: if not prop.nullable and not prop.default:
continue continue
prop_value = prop.default prop_value = prop.default
properties[prop_name] = prop.unmarshal(prop_value) 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) self._validate_properties(properties, one_of_schema=one_of_schema,
custom_formatters=custom_formatters)
return properties return properties
@ -313,12 +317,15 @@ class Schema(object):
SchemaType.NUMBER: self._validate_number, 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 value is None:
if not self.nullable: 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 return
# type validation # type validation
@ -326,20 +333,18 @@ class Schema(object):
self.type] self.type]
if not type_validator_callable(value): if not type_validator_callable(value):
raise InvalidSchemaValue( raise InvalidSchemaValue(
"Value of {0} not valid type of {1}".format( "Value {value} not valid type {type}", value, self.type.value)
value, self.type.value)
)
# structure validation # structure validation
validator_mapping = self.get_validator_mapping() validator_mapping = self.get_validator_mapping()
validator_callable = validator_mapping[self.type] validator_callable = validator_mapping[self.type]
validator_callable(value) validator_callable(value, custom_formatters=custom_formatters)
return value return value
def _validate_collection(self, value): def _validate_collection(self, value, custom_formatters=None):
if self.items is 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 is not None:
if self.min_items < 0: if self.min_items < 0:
@ -349,10 +354,8 @@ class Schema(object):
) )
if len(value) < self.min_items: if len(value) < self.min_items:
raise InvalidSchemaValue( raise InvalidSchemaValue(
"Value must contain at least {0} item(s)," "Value must contain at least {type} item(s),"
" {1} found".format( " {value} found", len(value), self.min_items)
self.min_items, len(value))
)
if self.max_items is not None: if self.max_items is not None:
if self.max_items < 0: if self.max_items < 0:
raise OpenAPISchemaError( raise OpenAPISchemaError(
@ -361,63 +364,55 @@ class Schema(object):
) )
if len(value) > self.max_items: if len(value) > self.max_items:
raise InvalidSchemaValue( raise InvalidSchemaValue(
"Value must contain at most {0} item(s)," "Value must contain at most {value} item(s),"
" {1} found".format( " {type} found", len(value), self.max_items)
self.max_items, len(value))
)
if self.unique_items and len(set(value)) != len(value): 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")
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): def _validate_number(self, value, custom_formatters=None):
if self.minimum is not None: if self.minimum is not None:
if self.exclusive_minimum and value <= self.minimum: if self.exclusive_minimum and value <= self.minimum:
raise InvalidSchemaValue( raise InvalidSchemaValue(
"Value {0} is not less than or equal to {1}".format( "Value {value} is not less than or equal to {type}", value, self.minimum)
value, self.minimum)
)
elif value < self.minimum: elif value < self.minimum:
raise InvalidSchemaValue( raise InvalidSchemaValue(
"Value {0} is not less than {1}".format( "Value {value} is not less than {type}", value, self.minimum)
value, self.minimum)
)
if self.maximum is not None: if self.maximum is not None:
if self.exclusive_maximum and value >= self.maximum: if self.exclusive_maximum and value >= self.maximum:
raise InvalidSchemaValue( raise InvalidSchemaValue(
"Value {0} is not greater than or equal to {1}".format( "Value {value} is not greater than or equal to {type}", value, self.maximum)
value, self.maximum)
)
elif value > self.maximum: elif value > self.maximum:
raise InvalidSchemaValue( raise InvalidSchemaValue(
"Value {0} is not greater than {1}".format( "Value {value} is not greater than {type}", value, self.maximum)
value, self.maximum)
)
if self.multiple_of is not None and value % self.multiple_of: if self.multiple_of is not None and value % self.multiple_of:
raise InvalidSchemaValue( raise InvalidSchemaValue(
"Value {0} is not a multiple of {1}".format( "Value {value} is not a multiple of {type}",
value, self.multiple_of) value, self.multiple_of)
)
def _validate_string(self, value): def _validate_string(self, value, custom_formatters=None):
try: try:
schema_format = SchemaFormat(self.format) schema_format = SchemaFormat(self.format)
except ValueError: except ValueError:
# @todo: implement custom format validation support msg = "Unsupported {0} format validation".format(self.format)
raise OpenAPISchemaError( if custom_formatters is not None:
"Unsupported {0} format validation".format(self.format) formatstring = custom_formatters.get(self.format)
) if formatstring is None:
raise OpenAPISchemaError(msg)
else:
raise OpenAPISchemaError(msg)
else: else:
format_validator_callable =\ formatstring =\
self.STRING_FORMAT_VALIDATOR_CALLABLE_GETTER[schema_format] self.STRING_FORMAT_CALLABLE_GETTER[schema_format]
if not format_validator_callable(value): if not formatstring.validate(value):
raise InvalidSchemaValue( raise InvalidSchemaValue(
"Value of {0} not valid format of {1}".format( "Value {value} not valid format {type}", value, self.format)
value, self.format)
)
if self.min_length is not None: if self.min_length is not None:
if self.min_length < 0: if self.min_length < 0:
@ -427,8 +422,8 @@ class Schema(object):
) )
if len(value) < self.min_length: if len(value) < self.min_length:
raise InvalidSchemaValue( raise InvalidSchemaValue(
"Value is shorter than the minimum length of {0}".format( "Value is shorter ({value}) than the minimum length of {type}",
self.min_length) len(value), self.min_length
) )
if self.max_length is not None: if self.max_length is not None:
if self.max_length < 0: if self.max_length < 0:
@ -438,40 +433,40 @@ class Schema(object):
) )
if len(value) > self.max_length: if len(value) > self.max_length:
raise InvalidSchemaValue( raise InvalidSchemaValue(
"Value is longer than the maximum length of {0}".format( "Value is longer ({value}) than the maximum length of {type}",
self.max_length) len(value), self.max_length
) )
if self.pattern is not None and not self.pattern.search(value): if self.pattern is not None and not self.pattern.search(value):
raise InvalidSchemaValue( raise InvalidSchemaValue(
"Value {0} does not match the pattern {1}".format( "Value {value} does not match the pattern {type}",
value, self.pattern.pattern) value, self.pattern.pattern
) )
return True return True
def _validate_object(self, value): def _validate_object(self, value, custom_formatters=None):
properties = value.__dict__ properties = value.__dict__
if self.one_of: if self.one_of:
valid_one_of_schema = None valid_one_of_schema = None
for one_of_schema in self.one_of: for one_of_schema in self.one_of:
try: try:
self._validate_properties(properties, one_of_schema) self._validate_properties(
properties, one_of_schema,
custom_formatters=custom_formatters)
except OpenAPISchemaError: except OpenAPISchemaError:
pass pass
else: else:
if valid_one_of_schema is not None: if valid_one_of_schema is not None:
raise MultipleOneOfSchema( raise MultipleOneOfSchema(self.type)
"Exactly one schema should be valid,"
"multiple found")
valid_one_of_schema = True valid_one_of_schema = True
if valid_one_of_schema is None: if valid_one_of_schema is None:
raise NoOneOfSchema( raise NoOneOfSchema(self.type)
"Exactly one valid schema should be valid, None found.")
else: else:
self._validate_properties(properties) self._validate_properties(properties,
custom_formatters=custom_formatters)
if self.min_properties is not None: if self.min_properties is not None:
if self.min_properties < 0: if self.min_properties < 0:
@ -482,9 +477,8 @@ class Schema(object):
if len(properties) < self.min_properties: if len(properties) < self.min_properties:
raise InvalidSchemaValue( raise InvalidSchemaValue(
"Value must contain at least {0} properties," "Value must contain at least {type} properties,"
" {1} found".format( " {value} found", len(properties), self.min_properties
self.min_properties, len(properties))
) )
if self.max_properties is not None: if self.max_properties is not None:
@ -495,14 +489,14 @@ class Schema(object):
) )
if len(properties) > self.max_properties: if len(properties) > self.max_properties:
raise InvalidSchemaValue( raise InvalidSchemaValue(
"Value must contain at most {0} properties," "Value must contain at most {type} properties,"
" {1} found".format( " {value} found", len(properties), self.max_properties
self.max_properties, len(properties))
) )
return True 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 = self.get_all_properties()
all_props_names = self.get_all_properties_names() all_props_names = self.get_all_properties_names()
all_req_props_names = self.get_all_required_properties_names() all_req_props_names = self.get_all_required_properties_names()
@ -517,24 +511,25 @@ class Schema(object):
value_props_names = value.keys() value_props_names = value.keys()
extra_props = set(value_props_names) - set(all_props_names) extra_props = set(value_props_names) - set(all_props_names)
if extra_props and self.additional_properties is None: if extra_props and self.additional_properties is None:
raise UndefinedSchemaProperty( raise UndefinedSchemaProperty(extra_props)
"Undefined properties in schema: {0}".format(extra_props))
for prop_name in extra_props: for prop_name in extra_props:
prop_value = value[prop_name] prop_value = value[prop_name]
self.additional_properties.validate( self.additional_properties.validate(
prop_value) prop_value, custom_formatters=custom_formatters)
for prop_name, prop in iteritems(all_props): for prop_name, prop in iteritems(all_props):
try: try:
prop_value = value[prop_name] prop_value = value[prop_name]
except KeyError: except KeyError:
if prop_name in all_req_props_names: if prop_name in all_req_props_names:
raise MissingSchemaProperty( raise MissingSchemaProperty(prop_name)
"Missing schema property {0}".format(prop_name))
if not prop.nullable and not prop.default: if not prop.nullable and not prop.default:
continue continue
prop_value = prop.default prop_value = prop.default
prop.validate(prop_value) try:
prop.validate(prop_value, custom_formatters=custom_formatters)
except OpenAPISchemaError as exc:
raise InvalidSchemaProperty(prop_name, original_exception=exc)
return True return True

View file

@ -1,9 +1,16 @@
from openapi_core.schema.exceptions import OpenAPIMappingError from openapi_core.schema.exceptions import OpenAPIMappingError
import attr
class OpenAPIServerError(OpenAPIMappingError): class OpenAPIServerError(OpenAPIMappingError):
pass pass
@attr.s
class InvalidServer(OpenAPIServerError): class InvalidServer(OpenAPIServerError):
pass full_url_pattern = attr.ib()
def __str__(self):
return "Invalid request server {0}".format(
self.full_url_pattern)

View file

@ -31,8 +31,7 @@ class Spec(object):
if spec_server.default_url in full_url_pattern: if spec_server.default_url in full_url_pattern:
return spec_server return spec_server
raise InvalidServer( raise InvalidServer(full_url_pattern)
"Invalid request server {0}".format(full_url_pattern))
def get_server_url(self, index=0): def get_server_url(self, index=0):
return self.servers[index].default_url return self.servers[index].default_url
@ -41,9 +40,7 @@ class Spec(object):
try: try:
return self.paths[path_pattern].operations[http_method] return self.paths[path_pattern].operations[http_method]
except KeyError: except KeyError:
raise InvalidOperation( raise InvalidOperation(path_pattern, http_method)
"Unknown operation path {0} with method {1}".format(
path_pattern, http_method))
def get_schema(self, name): def get_schema(self, name):
return self.components.schemas[name] return self.components.schemas[name]

View file

@ -11,8 +11,9 @@ from openapi_core.validation.util import get_operation_pattern
class RequestValidator(object): class RequestValidator(object):
def __init__(self, spec): def __init__(self, spec, custom_formatters=None):
self.spec = spec self.spec = spec
self.custom_formatters = custom_formatters
def validate(self, request): def validate(self, request):
try: try:
@ -52,7 +53,7 @@ class RequestValidator(object):
continue continue
try: try:
value = param.unmarshal(raw_value) value = param.unmarshal(raw_value, self.custom_formatters)
except OpenAPIMappingError as exc: except OpenAPIMappingError as exc:
errors.append(exc) errors.append(exc)
else: else:
@ -78,7 +79,7 @@ class RequestValidator(object):
errors.append(exc) errors.append(exc)
else: else:
try: try:
body = media_type.unmarshal(raw_body) body = media_type.unmarshal(raw_body, self.custom_formatters)
except OpenAPIMappingError as exc: except OpenAPIMappingError as exc:
errors.append(exc) errors.append(exc)

View file

@ -6,8 +6,9 @@ from openapi_core.validation.util import get_operation_pattern
class ResponseValidator(object): class ResponseValidator(object):
def __init__(self, spec): def __init__(self, spec, custom_formatters=None):
self.spec = spec self.spec = spec
self.custom_formatters = custom_formatters
def validate(self, request, response): def validate(self, request, response):
try: try:
@ -60,7 +61,7 @@ class ResponseValidator(object):
errors.append(exc) errors.append(exc)
else: else:
try: try:
data = media_type.unmarshal(raw_data) data = media_type.unmarshal(raw_data, self.custom_formatters)
except OpenAPIMappingError as exc: except OpenAPIMappingError as exc:
errors.append(exc) errors.append(exc)

View file

@ -1,3 +1,4 @@
openapi-spec-validator openapi-spec-validator
six six
lazy-object-proxy lazy-object-proxy
attrs

View file

@ -4,3 +4,4 @@ lazy-object-proxy
backports.functools-lru-cache backports.functools-lru-cache
backports.functools-partialmethod backports.functools-partialmethod
enum34 enum34
attrs

View file

@ -44,6 +44,7 @@ class PyTest(TestCommand):
'--cov', 'openapi_core', '--cov', 'openapi_core',
'--cov-report', 'term-missing', '--cov-report', 'term-missing',
'--cov-report', 'xml:reports/coverage.xml', '--cov-report', 'xml:reports/coverage.xml',
'tests',
] ]
self.test_suite = True self.test_suite = True

View file

@ -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.request_bodies.models import RequestBody
from openapi_core.schema.responses.models import Response from openapi_core.schema.responses.models import Response
from openapi_core.schema.schemas.exceptions import ( from openapi_core.schema.schemas.exceptions import (
UndefinedSchemaProperty, MissingSchemaProperty, NoOneOfSchema,
NoValidSchema, NoValidSchema,
) )
from openapi_core.schema.schemas.models import Schema 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) request.get_body(spec)
def test_post_cats_only_required_body(self, spec, spec_dict): def test_post_cats_only_required_body(self, spec, spec_dict):
@ -952,7 +951,7 @@ class TestPetstore(object):
assert parameters == {} assert parameters == {}
with pytest.raises(UndefinedSchemaProperty): with pytest.raises(InvalidMediaTypeValue):
request.get_body(spec) request.get_body(spec)
def test_post_tags_empty_body(self, spec, spec_dict): def test_post_tags_empty_body(self, spec, spec_dict):
@ -970,7 +969,7 @@ class TestPetstore(object):
assert parameters == {} assert parameters == {}
with pytest.raises(MissingSchemaProperty): with pytest.raises(InvalidMediaTypeValue):
request.get_body(spec) request.get_body(spec)
def test_post_tags_wrong_property_type(self, spec): def test_post_tags_wrong_property_type(self, spec):