mirror of
https://github.com/correl/openapi-core.git
synced 2024-11-24 19:19:56 +00:00
Merge pull request #94 from domenkozar/structured-exceptions
Structured exceptions
This commit is contained in:
commit
2e6f2dedc9
22 changed files with 305 additions and 204 deletions
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = {
|
||||
|
@ -32,21 +32,21 @@ 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
|
||||
|
||||
try:
|
||||
deserialized = self.deserialize(value)
|
||||
except ValueError as exc:
|
||||
raise InvalidMediaTypeValue(str(exc))
|
||||
raise InvalidMediaTypeValue(exc)
|
||||
|
||||
try:
|
||||
unmarshalled = self.schema.unmarshal(deserialized)
|
||||
except InvalidSchemaValue as exc:
|
||||
raise InvalidMediaTypeValue(str(exc))
|
||||
unmarshalled = self.schema.unmarshal(deserialized, custom_formatters=custom_formatters)
|
||||
except OpenAPISchemaError as exc:
|
||||
raise InvalidMediaTypeValue(exc)
|
||||
|
||||
try:
|
||||
return self.schema.validate(unmarshalled)
|
||||
except InvalidSchemaValue as exc:
|
||||
raise InvalidMediaTypeValue(str(exc))
|
||||
return self.schema.validate(unmarshalled, custom_formatters=custom_formatters)
|
||||
except OpenAPISchemaError as exc:
|
||||
raise InvalidMediaTypeValue(exc)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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(self.name, self.original_exception)
|
||||
|
|
|
@ -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__)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -91,7 +89,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),
|
||||
|
@ -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)
|
||||
except InvalidSchemaValue as exc:
|
||||
raise InvalidParameterValue(str(exc))
|
||||
unmarshalled = self.schema.unmarshal(deserialized, custom_formatters=custom_formatters)
|
||||
except OpenAPISchemaError as exc:
|
||||
raise InvalidParameterValue(self.name, exc)
|
||||
|
||||
try:
|
||||
return self.schema.validate(unmarshalled)
|
||||
except InvalidSchemaValue as exc:
|
||||
raise InvalidParameterValue(str(exc))
|
||||
return self.schema.validate(unmarshalled, custom_formatters=custom_formatters)
|
||||
except OpenAPISchemaError as exc:
|
||||
raise InvalidParameterValue(self.name, exc)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,33 +1,79 @@
|
|||
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 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):
|
||||
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)
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
"""OpenAPI core schemas models module"""
|
||||
import attr
|
||||
import functools
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
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 (
|
||||
InvalidSchemaValue, UndefinedSchemaProperty, MissingSchemaProperty,
|
||||
OpenAPISchemaError, NoOneOfSchema, MultipleOneOfSchema, NoValidSchema,
|
||||
UndefinedItemsSchema,
|
||||
UndefinedItemsSchema, InvalidCustomFormatSchemaValue, InvalidSchemaProperty,
|
||||
)
|
||||
from openapi_core.schema.schemas.util import (
|
||||
forcebool, format_date, format_datetime,
|
||||
|
@ -24,6 +26,12 @@ from openapi_core.schema.schemas.validators import (
|
|||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@attr.s
|
||||
class Format(object):
|
||||
unmarshal = attr.ib()
|
||||
validate = attr.ib()
|
||||
|
||||
|
||||
class Schema(object):
|
||||
"""Represents an OpenAPI Schema."""
|
||||
|
||||
|
@ -33,18 +41,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: 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 = {
|
||||
|
@ -142,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")
|
||||
raise InvalidSchemaValue("Null value for non-nullable schema", value, self.type)
|
||||
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
|
||||
|
@ -170,47 +173,45 @@ 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):
|
||||
def unmarshal(self, value, custom_formatters=None):
|
||||
"""Unmarshal parameter from the value."""
|
||||
if self.deprecated:
|
||||
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:
|
||||
return None
|
||||
|
||||
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
|
||||
|
||||
def _unmarshal_string(self, value):
|
||||
def _unmarshal_string(self, value, custom_formatters=None):
|
||||
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 format {type} unmarshalling for value {value}"
|
||||
if custom_formatters is not None:
|
||||
formatstring = custom_formatters.get(self.format)
|
||||
if formatstring is None:
|
||||
raise InvalidSchemaValue(msg, value, self.format)
|
||||
else:
|
||||
formatter = self.STRING_FORMAT_CAST_CALLABLE_GETTER[schema_format]
|
||||
raise InvalidSchemaValue(msg, value, self.format)
|
||||
else:
|
||||
formatstring = self.STRING_FORMAT_CALLABLE_GETTER[schema_format]
|
||||
|
||||
try:
|
||||
return formatter(value)
|
||||
except ValueError:
|
||||
raise InvalidSchemaValue(
|
||||
"Failed to format value of {0} to {1}".format(
|
||||
value, self.format)
|
||||
)
|
||||
return formatstring.unmarshal(value)
|
||||
except ValueError as exc:
|
||||
raise InvalidCustomFormatSchemaValue(
|
||||
"Failed to format value {value} to format {type}: {exception}", value, self.format, exc)
|
||||
|
||||
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,
|
||||
|
@ -224,19 +225,20 @@ 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):
|
||||
def _unmarshal_collection(self, value, custom_formatters=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, )):
|
||||
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()
|
||||
|
||||
|
@ -245,26 +247,25 @@ 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:
|
||||
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(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()
|
||||
|
@ -279,28 +280,31 @@ 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:
|
||||
prop_value = value[prop_name]
|
||||
properties[prop_name] = self.additional_properties.unmarshal(
|
||||
prop_value)
|
||||
prop_value, custom_formatters=custom_formatters)
|
||||
|
||||
for prop_name, prop in iteritems(all_props):
|
||||
try:
|
||||
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
|
||||
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
|
||||
|
||||
|
@ -313,12 +317,15 @@ 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")
|
||||
raise InvalidSchemaValue("Null value for non-nullable schema of type {type}", value, self.type)
|
||||
return
|
||||
|
||||
# type validation
|
||||
|
@ -326,20 +333,18 @@ 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()
|
||||
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")
|
||||
raise UndefinedItemsSchema(self.type)
|
||||
|
||||
if self.min_items is not None:
|
||||
if self.min_items < 0:
|
||||
|
@ -349,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(
|
||||
|
@ -361,63 +364,55 @@ 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")
|
||||
|
||||
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.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):
|
||||
def _validate_string(self, value, custom_formatters=None):
|
||||
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 custom_formatters is not None:
|
||||
formatstring = custom_formatters.get(self.format)
|
||||
if formatstring is None:
|
||||
raise OpenAPISchemaError(msg)
|
||||
else:
|
||||
format_validator_callable =\
|
||||
self.STRING_FORMAT_VALIDATOR_CALLABLE_GETTER[schema_format]
|
||||
raise OpenAPISchemaError(msg)
|
||||
else:
|
||||
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)
|
||||
)
|
||||
"Value {value} not valid format {type}", value, self.format)
|
||||
|
||||
if self.min_length is not None:
|
||||
if self.min_length < 0:
|
||||
|
@ -427,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:
|
||||
|
@ -438,40 +433,40 @@ 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
|
||||
|
||||
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:
|
||||
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)
|
||||
self._validate_properties(properties,
|
||||
custom_formatters=custom_formatters)
|
||||
|
||||
if self.min_properties is not None:
|
||||
if self.min_properties < 0:
|
||||
|
@ -482,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:
|
||||
|
@ -495,14 +489,14 @@ 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
|
||||
|
||||
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()
|
||||
|
@ -517,24 +511,25 @@ 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]
|
||||
self.additional_properties.validate(
|
||||
prop_value)
|
||||
prop_value, custom_formatters=custom_formatters)
|
||||
|
||||
for prop_name, prop in iteritems(all_props):
|
||||
try:
|
||||
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
|
||||
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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
openapi-spec-validator
|
||||
six
|
||||
lazy-object-proxy
|
||||
attrs
|
||||
|
|
|
@ -4,3 +4,4 @@ lazy-object-proxy
|
|||
backports.functools-lru-cache
|
||||
backports.functools-partialmethod
|
||||
enum34
|
||||
attrs
|
||||
|
|
1
setup.py
1
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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in a new issue