2018-04-17 12:18:40 +00:00
|
|
|
"""OpenAPI core schemas models module"""
|
2017-09-21 11:51:37 +00:00
|
|
|
import logging
|
|
|
|
from collections import defaultdict
|
2018-08-22 10:51:06 +00:00
|
|
|
from datetime import date, datetime
|
2017-10-17 13:23:26 +00:00
|
|
|
import warnings
|
|
|
|
|
2018-08-17 14:54:01 +00:00
|
|
|
from six import iteritems, integer_types, binary_type, text_type
|
2017-09-21 11:51:37 +00:00
|
|
|
|
2018-04-17 12:18:40 +00:00
|
|
|
from openapi_core.extensions.models.factories import ModelFactory
|
2018-08-17 04:25:12 +00:00
|
|
|
from openapi_core.schema.schemas.enums import SchemaFormat, SchemaType
|
2018-04-18 10:39:03 +00:00
|
|
|
from openapi_core.schema.schemas.exceptions import (
|
|
|
|
InvalidSchemaValue, UndefinedSchemaProperty, MissingSchemaProperty,
|
2018-08-05 12:40:34 +00:00
|
|
|
OpenAPISchemaError, NoOneOfSchema, MultipleOneOfSchema, NoValidSchema,
|
|
|
|
UndefinedItemsSchema,
|
2018-04-18 10:39:03 +00:00
|
|
|
)
|
2018-08-22 10:51:06 +00:00
|
|
|
from openapi_core.schema.schemas.util import (
|
|
|
|
forcebool, format_date, format_datetime,
|
|
|
|
)
|
2018-08-17 14:54:01 +00:00
|
|
|
from openapi_core.schema.schemas.validators import (
|
|
|
|
TypeValidator, AttributeValidator,
|
|
|
|
)
|
2017-09-22 08:14:07 +00:00
|
|
|
|
2017-09-21 11:51:37 +00:00
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
2018-02-28 12:01:05 +00:00
|
|
|
|
2017-09-21 11:51:37 +00:00
|
|
|
class Schema(object):
|
|
|
|
"""Represents an OpenAPI Schema."""
|
|
|
|
|
2018-04-17 12:18:40 +00:00
|
|
|
DEFAULT_CAST_CALLABLE_GETTER = {
|
|
|
|
SchemaType.INTEGER: int,
|
|
|
|
SchemaType.NUMBER: float,
|
|
|
|
SchemaType.BOOLEAN: forcebool,
|
|
|
|
}
|
|
|
|
|
2018-08-22 10:51:06 +00:00
|
|
|
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),
|
|
|
|
}
|
2018-08-17 04:25:12 +00:00
|
|
|
|
2018-08-22 09:05:15 +00:00
|
|
|
TYPE_VALIDATOR_CALLABLE_GETTER = {
|
2018-08-05 12:40:34 +00:00
|
|
|
SchemaType.ANY: lambda x: x,
|
2018-08-17 14:54:01 +00:00
|
|
|
SchemaType.BOOLEAN: TypeValidator(bool),
|
|
|
|
SchemaType.INTEGER: TypeValidator(integer_types, exclude=bool),
|
|
|
|
SchemaType.NUMBER: TypeValidator(integer_types, float, exclude=bool),
|
2018-08-22 10:51:06 +00:00
|
|
|
SchemaType.STRING: TypeValidator(
|
|
|
|
text_type, date, datetime, binary_type),
|
2018-08-17 14:54:01 +00:00
|
|
|
SchemaType.ARRAY: TypeValidator(list, tuple),
|
2018-08-22 09:05:15 +00:00
|
|
|
SchemaType.OBJECT: AttributeValidator('__dict__'),
|
2018-08-17 14:54:01 +00:00
|
|
|
}
|
|
|
|
|
2017-09-21 11:51:37 +00:00
|
|
|
def __init__(
|
2018-04-04 10:26:21 +00:00
|
|
|
self, schema_type=None, model=None, properties=None, items=None,
|
2017-11-14 13:36:05 +00:00
|
|
|
schema_format=None, required=None, default=None, nullable=False,
|
2018-05-30 10:15:17 +00:00
|
|
|
enum=None, deprecated=False, all_of=None, one_of=None,
|
|
|
|
additional_properties=None):
|
2018-08-02 18:30:51 +00:00
|
|
|
self.type = SchemaType(schema_type)
|
2017-09-22 08:14:07 +00:00
|
|
|
self.model = model
|
2017-09-21 11:51:37 +00:00
|
|
|
self.properties = properties and dict(properties) or {}
|
|
|
|
self.items = items
|
2018-05-30 08:41:34 +00:00
|
|
|
self.format = schema_format
|
2017-11-06 16:50:00 +00:00
|
|
|
self.required = required or []
|
2017-09-25 14:15:00 +00:00
|
|
|
self.default = default
|
2017-10-17 13:02:21 +00:00
|
|
|
self.nullable = nullable
|
2017-10-17 13:23:26 +00:00
|
|
|
self.enum = enum
|
2017-10-17 13:33:46 +00:00
|
|
|
self.deprecated = deprecated
|
2017-11-06 16:50:00 +00:00
|
|
|
self.all_of = all_of and list(all_of) or []
|
2018-05-25 15:32:09 +00:00
|
|
|
self.one_of = one_of and list(one_of) or []
|
2018-05-30 10:15:17 +00:00
|
|
|
self.additional_properties = additional_properties
|
2018-05-25 15:32:09 +00:00
|
|
|
|
|
|
|
self._all_required_properties_cache = None
|
|
|
|
self._all_optional_properties_cache = None
|
2017-09-21 11:51:37 +00:00
|
|
|
|
|
|
|
def __getitem__(self, name):
|
|
|
|
return self.properties[name]
|
|
|
|
|
2017-11-06 16:50:00 +00:00
|
|
|
def get_all_properties(self):
|
|
|
|
properties = self.properties.copy()
|
|
|
|
|
|
|
|
for subschema in self.all_of:
|
|
|
|
subschema_props = subschema.get_all_properties()
|
|
|
|
properties.update(subschema_props)
|
|
|
|
|
|
|
|
return properties
|
|
|
|
|
2018-05-25 15:32:09 +00:00
|
|
|
def get_all_properties_names(self):
|
|
|
|
all_properties = self.get_all_properties()
|
|
|
|
return set(all_properties.keys())
|
|
|
|
|
2018-04-04 14:15:45 +00:00
|
|
|
def get_all_required_properties(self):
|
2018-05-25 15:32:09 +00:00
|
|
|
if self._all_required_properties_cache is None:
|
|
|
|
self._all_required_properties_cache =\
|
|
|
|
self._get_all_required_properties()
|
|
|
|
|
|
|
|
return self._all_required_properties_cache
|
|
|
|
|
|
|
|
def _get_all_required_properties(self):
|
|
|
|
all_properties = self.get_all_properties()
|
|
|
|
required = self.get_all_required_properties_names()
|
|
|
|
|
|
|
|
return dict(
|
|
|
|
(prop_name, val)
|
2018-08-05 12:40:34 +00:00
|
|
|
for prop_name, val in iteritems(all_properties)
|
2018-05-25 15:32:09 +00:00
|
|
|
if prop_name in required
|
|
|
|
)
|
|
|
|
|
|
|
|
def get_all_required_properties_names(self):
|
2018-07-15 21:22:44 +00:00
|
|
|
required = self.required[:]
|
2018-04-04 14:15:45 +00:00
|
|
|
|
|
|
|
for subschema in self.all_of:
|
|
|
|
subschema_req = subschema.get_all_required_properties()
|
|
|
|
required += subschema_req
|
|
|
|
|
2018-05-25 15:32:09 +00:00
|
|
|
return set(required)
|
2018-04-04 14:15:45 +00:00
|
|
|
|
2017-09-21 11:51:37 +00:00
|
|
|
def get_cast_mapping(self):
|
2018-04-17 12:18:40 +00:00
|
|
|
mapping = self.DEFAULT_CAST_CALLABLE_GETTER.copy()
|
2017-09-22 08:14:07 +00:00
|
|
|
mapping.update({
|
2018-08-17 04:25:12 +00:00
|
|
|
SchemaType.STRING: self._unmarshal_string,
|
2018-08-05 12:40:34 +00:00
|
|
|
SchemaType.ANY: self._unmarshal_any,
|
2017-11-14 13:36:05 +00:00
|
|
|
SchemaType.ARRAY: self._unmarshal_collection,
|
|
|
|
SchemaType.OBJECT: self._unmarshal_object,
|
2017-09-22 08:14:07 +00:00
|
|
|
})
|
2017-09-21 11:51:37 +00:00
|
|
|
|
|
|
|
return defaultdict(lambda: lambda x: x, mapping)
|
|
|
|
|
|
|
|
def cast(self, value):
|
|
|
|
"""Cast value to schema type"""
|
|
|
|
if value is None:
|
2017-10-17 13:02:21 +00:00
|
|
|
if not self.nullable:
|
2018-04-18 10:39:03 +00:00
|
|
|
raise InvalidSchemaValue("Null value for non-nullable schema")
|
2017-10-17 13:02:21 +00:00
|
|
|
return self.default
|
2017-09-21 11:51:37 +00:00
|
|
|
|
|
|
|
cast_mapping = self.get_cast_mapping()
|
|
|
|
|
2018-08-17 04:25:12 +00:00
|
|
|
if self.type is not SchemaType.STRING and value == '':
|
2017-09-21 11:51:37 +00:00
|
|
|
return None
|
|
|
|
|
|
|
|
cast_callable = cast_mapping[self.type]
|
|
|
|
try:
|
|
|
|
return cast_callable(value)
|
|
|
|
except ValueError:
|
2018-04-18 10:39:03 +00:00
|
|
|
raise InvalidSchemaValue(
|
2017-11-03 11:18:48 +00:00
|
|
|
"Failed to cast value of {0} to {1}".format(value, self.type)
|
2017-09-21 11:51:37 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
def unmarshal(self, value):
|
|
|
|
"""Unmarshal parameter from the value."""
|
2017-10-17 13:33:46 +00:00
|
|
|
if self.deprecated:
|
2018-08-05 12:40:34 +00:00
|
|
|
warnings.warn("The schema is deprecated", DeprecationWarning)
|
|
|
|
|
2017-09-21 11:51:37 +00:00
|
|
|
casted = self.cast(value)
|
|
|
|
|
|
|
|
if casted is None and not self.required:
|
|
|
|
return None
|
|
|
|
|
2017-10-17 13:23:26 +00:00
|
|
|
if self.enum and casted not in self.enum:
|
2018-04-18 10:39:03 +00:00
|
|
|
raise InvalidSchemaValue(
|
2017-11-03 11:18:48 +00:00
|
|
|
"Value of {0} not in enum choices: {1}".format(
|
|
|
|
value, self.enum)
|
2017-10-17 13:23:26 +00:00
|
|
|
)
|
|
|
|
|
2017-09-21 11:51:37 +00:00
|
|
|
return casted
|
|
|
|
|
2018-08-17 04:25:12 +00:00
|
|
|
def _unmarshal_string(self, value):
|
2018-08-22 10:51:06 +00:00
|
|
|
try:
|
|
|
|
schema_format = SchemaFormat(self.format)
|
|
|
|
except ValueError:
|
|
|
|
# @todo: implement custom format unmarshalling support
|
|
|
|
raise OpenAPISchemaError(
|
|
|
|
"Unsupported {0} format unmarshalling".format(self.format)
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
formatter = self.STRING_FORMAT_CAST_CALLABLE_GETTER[schema_format]
|
|
|
|
|
2018-08-17 04:25:12 +00:00
|
|
|
try:
|
|
|
|
return formatter(value)
|
|
|
|
except ValueError:
|
|
|
|
raise InvalidSchemaValue(
|
|
|
|
"Failed to format value of {0} to {1}".format(
|
|
|
|
value, self.format)
|
|
|
|
)
|
|
|
|
|
2018-08-05 12:40:34 +00:00
|
|
|
def _unmarshal_any(self, value):
|
|
|
|
types_resolve_order = [
|
|
|
|
SchemaType.OBJECT, SchemaType.ARRAY, SchemaType.BOOLEAN,
|
|
|
|
SchemaType.INTEGER, SchemaType.NUMBER, SchemaType.STRING,
|
|
|
|
]
|
|
|
|
cast_mapping = self.get_cast_mapping()
|
|
|
|
for schema_type in types_resolve_order:
|
|
|
|
cast_callable = cast_mapping[schema_type]
|
|
|
|
try:
|
|
|
|
return cast_callable(value)
|
|
|
|
# @todo: remove ValueError when validation separated
|
|
|
|
except (OpenAPISchemaError, TypeError, ValueError):
|
|
|
|
continue
|
|
|
|
|
|
|
|
raise NoValidSchema(
|
|
|
|
"No valid schema found for value {0}".format(value))
|
|
|
|
|
2017-09-22 08:14:07 +00:00
|
|
|
def _unmarshal_collection(self, value):
|
2018-08-05 12:40:34 +00:00
|
|
|
if self.items is None:
|
|
|
|
raise UndefinedItemsSchema("Undefined items' schema")
|
|
|
|
|
2017-09-22 08:14:07 +00:00
|
|
|
return list(map(self.items.unmarshal, value))
|
|
|
|
|
2018-08-21 17:33:24 +00:00
|
|
|
def _unmarshal_object(self, value, model_factory=None):
|
2018-04-23 18:50:29 +00:00
|
|
|
if not isinstance(value, (dict, )):
|
2018-04-18 10:39:03 +00:00
|
|
|
raise InvalidSchemaValue(
|
2018-08-21 17:33:24 +00:00
|
|
|
"Value of {0} not a dict".format(value))
|
|
|
|
|
|
|
|
model_factory = model_factory or ModelFactory()
|
2017-09-22 08:14:07 +00:00
|
|
|
|
2018-05-25 15:32:09 +00:00
|
|
|
if self.one_of:
|
|
|
|
properties = None
|
|
|
|
for one_of_schema in self.one_of:
|
|
|
|
try:
|
|
|
|
found_props = self._unmarshal_properties(
|
|
|
|
value, one_of_schema)
|
|
|
|
except OpenAPISchemaError:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
if properties is not None:
|
|
|
|
raise MultipleOneOfSchema(
|
|
|
|
"Exactly one schema should be valid,"
|
|
|
|
"multiple found")
|
|
|
|
properties = found_props
|
|
|
|
|
|
|
|
if properties is None:
|
|
|
|
raise NoOneOfSchema(
|
|
|
|
"Exactly one valid schema should be valid, None found.")
|
|
|
|
|
|
|
|
else:
|
|
|
|
properties = self._unmarshal_properties(value)
|
|
|
|
|
2018-08-21 17:33:24 +00:00
|
|
|
return model_factory.create(properties, name=self.model)
|
2017-09-25 14:15:00 +00:00
|
|
|
|
2018-05-25 15:32:09 +00:00
|
|
|
def _unmarshal_properties(self, value, one_of_schema=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()
|
2017-09-25 14:15:00 +00:00
|
|
|
|
2018-05-25 15:32:09 +00:00
|
|
|
if one_of_schema is not None:
|
|
|
|
all_props.update(one_of_schema.get_all_properties())
|
|
|
|
all_props_names |= one_of_schema.\
|
|
|
|
get_all_properties_names()
|
|
|
|
all_req_props_names |= one_of_schema.\
|
|
|
|
get_all_required_properties_names()
|
|
|
|
|
|
|
|
value_props_names = value.keys()
|
|
|
|
extra_props = set(value_props_names) - set(all_props_names)
|
2018-05-30 10:15:17 +00:00
|
|
|
if extra_props and self.additional_properties is None:
|
2017-09-25 14:15:00 +00:00
|
|
|
raise UndefinedSchemaProperty(
|
|
|
|
"Undefined properties in schema: {0}".format(extra_props))
|
|
|
|
|
2017-09-22 08:14:07 +00:00
|
|
|
properties = {}
|
2018-05-30 10:15:17 +00:00
|
|
|
for prop_name in extra_props:
|
|
|
|
prop_value = value[prop_name]
|
|
|
|
properties[prop_name] = self.additional_properties.unmarshal(
|
|
|
|
prop_value)
|
|
|
|
|
2018-05-25 15:32:09 +00:00
|
|
|
for prop_name, prop in iteritems(all_props):
|
2017-09-25 14:15:00 +00:00
|
|
|
try:
|
|
|
|
prop_value = value[prop_name]
|
|
|
|
except KeyError:
|
2018-05-25 15:32:09 +00:00
|
|
|
if prop_name in all_req_props_names:
|
2018-04-18 10:39:03 +00:00
|
|
|
raise MissingSchemaProperty(
|
2017-09-25 14:15:00 +00:00
|
|
|
"Missing schema property {0}".format(prop_name))
|
2017-10-17 13:02:21 +00:00
|
|
|
if not prop.nullable and not prop.default:
|
|
|
|
continue
|
2017-09-25 14:15:00 +00:00
|
|
|
prop_value = prop.default
|
2017-09-22 08:14:07 +00:00
|
|
|
properties[prop_name] = prop.unmarshal(prop_value)
|
2018-08-21 17:33:24 +00:00
|
|
|
|
|
|
|
self._validate_properties(properties, one_of_schema=one_of_schema)
|
|
|
|
|
2018-05-25 15:32:09 +00:00
|
|
|
return properties
|
2018-08-17 14:54:01 +00:00
|
|
|
|
2018-08-21 17:33:24 +00:00
|
|
|
def get_validator_mapping(self):
|
2018-08-22 09:05:15 +00:00
|
|
|
mapping = {
|
2018-08-22 09:29:39 +00:00
|
|
|
SchemaType.ARRAY: self._validate_collection,
|
2018-08-22 10:51:06 +00:00
|
|
|
SchemaType.STRING: self._validate_string,
|
2018-08-21 17:33:24 +00:00
|
|
|
SchemaType.OBJECT: self._validate_object,
|
2018-08-22 09:05:15 +00:00
|
|
|
}
|
2018-08-21 17:33:24 +00:00
|
|
|
|
|
|
|
return defaultdict(lambda: lambda x: x, mapping)
|
|
|
|
|
2018-08-17 14:54:01 +00:00
|
|
|
def validate(self, value):
|
|
|
|
if value is None:
|
|
|
|
if not self.nullable:
|
|
|
|
raise InvalidSchemaValue("Null value for non-nullable schema")
|
2018-08-22 09:05:15 +00:00
|
|
|
return
|
2018-08-17 14:54:01 +00:00
|
|
|
|
2018-08-22 09:05:15 +00:00
|
|
|
# type validation
|
|
|
|
type_validator_callable = self.TYPE_VALIDATOR_CALLABLE_GETTER[
|
|
|
|
self.type]
|
|
|
|
if not type_validator_callable(value):
|
2018-08-17 14:54:01 +00:00
|
|
|
raise InvalidSchemaValue(
|
|
|
|
"Value of {0} not valid type of {1}".format(
|
|
|
|
value, self.type.value)
|
|
|
|
)
|
|
|
|
|
2018-08-22 09:05:15 +00:00
|
|
|
# structure validation
|
|
|
|
validator_mapping = self.get_validator_mapping()
|
|
|
|
validator_callable = validator_mapping[self.type]
|
|
|
|
validator_callable(value)
|
|
|
|
|
2018-08-17 14:54:01 +00:00
|
|
|
return value
|
2018-08-21 17:33:24 +00:00
|
|
|
|
2018-08-22 09:29:39 +00:00
|
|
|
def _validate_collection(self, value):
|
|
|
|
if self.items is None:
|
|
|
|
raise OpenAPISchemaError("Schema for collection not defined")
|
|
|
|
|
|
|
|
return list(map(self.items.validate, value))
|
|
|
|
|
2018-08-22 10:51:06 +00:00
|
|
|
def _validate_string(self, value):
|
|
|
|
try:
|
|
|
|
schema_format = SchemaFormat(self.format)
|
|
|
|
except ValueError:
|
|
|
|
# @todo: implement custom format validation support
|
|
|
|
raise OpenAPISchemaError(
|
|
|
|
"Unsupported {0} format validation".format(self.format)
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
format_validator_callable =\
|
|
|
|
self.STRING_FORMAT_VALIDATOR_CALLABLE_GETTER[schema_format]
|
|
|
|
|
|
|
|
if not format_validator_callable(value):
|
|
|
|
raise InvalidSchemaValue(
|
|
|
|
"Value of {0} not valid format of {1}".format(
|
|
|
|
value, self.format)
|
|
|
|
)
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
2018-08-21 17:33:24 +00:00
|
|
|
def _validate_object(self, value):
|
|
|
|
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)
|
|
|
|
except OpenAPISchemaError:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
if valid_one_of_schema is not None:
|
|
|
|
raise MultipleOneOfSchema(
|
|
|
|
"Exactly one schema should be valid,"
|
|
|
|
"multiple found")
|
|
|
|
valid_one_of_schema = True
|
|
|
|
|
|
|
|
if valid_one_of_schema is None:
|
|
|
|
raise NoOneOfSchema(
|
|
|
|
"Exactly one valid schema should be valid, None found.")
|
|
|
|
|
|
|
|
else:
|
|
|
|
self._validate_properties(properties)
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
def _validate_properties(self, value, one_of_schema=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()
|
|
|
|
|
|
|
|
if one_of_schema is not None:
|
|
|
|
all_props.update(one_of_schema.get_all_properties())
|
|
|
|
all_props_names |= one_of_schema.\
|
|
|
|
get_all_properties_names()
|
|
|
|
all_req_props_names |= one_of_schema.\
|
|
|
|
get_all_required_properties_names()
|
|
|
|
|
|
|
|
value_props_names = value.keys()
|
|
|
|
extra_props = set(value_props_names) - set(all_props_names)
|
|
|
|
if extra_props and self.additional_properties is None:
|
|
|
|
raise UndefinedSchemaProperty(
|
|
|
|
"Undefined properties in schema: {0}".format(extra_props))
|
|
|
|
|
|
|
|
for prop_name in extra_props:
|
|
|
|
prop_value = value[prop_name]
|
|
|
|
self.additional_properties.validate(
|
|
|
|
prop_value)
|
|
|
|
|
|
|
|
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))
|
|
|
|
if not prop.nullable and not prop.default:
|
|
|
|
continue
|
|
|
|
prop_value = prop.default
|
|
|
|
prop.validate(prop_value)
|
|
|
|
|
|
|
|
return True
|