mirror of
https://github.com/correl/openapi-core.git
synced 2024-11-25 11:09:53 +00:00
56be4b10eb
Currently if valid swagger syntax is used for model composition an error will be thrown due to the lack of a type property. This was corrected by making object the default type. schema_type = schema_deref.get('type', 'object') I changed the swagger definition to test for this. Now PetCreate is a composite of PetCreatePartOne and PetCreatePartTwo. However, this caused `test_post_pets_empty_body` to fail, which turned out to be a bug in the required properties. In `_unmarshal_object` the `get_all_properties` method is called to get all properties from the subschemas. However, this is not done for required properties, meaning that only top level required properties will be correctly validated. I have added a `get_all_required_properties’ to fix this. This caused `test_get_pets` to fail. In this case the bug allowed an incorrect test case to be introduced. Pet requires `id`, but it also requires name because it inherits from PetCreate. I have fixed this test case by adding the missing required property. After these changes `test_get_pet_not_found` failed due to a string formatting error (double quotes vs single quotes). I fixed this by switching to dictionary comparisons.
247 lines
7.6 KiB
Python
247 lines
7.6 KiB
Python
"""OpenAPI core schemas module"""
|
|
import logging
|
|
from collections import defaultdict
|
|
import warnings
|
|
|
|
from distutils.util import strtobool
|
|
from functools import lru_cache
|
|
|
|
from json import loads
|
|
from six import iteritems
|
|
|
|
from openapi_core.enums import SchemaType, SchemaFormat
|
|
from openapi_core.exceptions import (
|
|
InvalidValueType, UndefinedSchemaProperty, MissingProperty, InvalidValue,
|
|
)
|
|
from openapi_core.models import ModelFactory
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
def forcebool(val):
|
|
if isinstance(val, str):
|
|
val = strtobool(val)
|
|
|
|
return bool(val)
|
|
|
|
|
|
DEFAULT_CAST_CALLABLE_GETTER = {
|
|
SchemaType.INTEGER: int,
|
|
SchemaType.NUMBER: float,
|
|
SchemaType.BOOLEAN: forcebool,
|
|
}
|
|
|
|
|
|
class Schema(object):
|
|
"""Represents an OpenAPI Schema."""
|
|
|
|
def __init__(
|
|
self, schema_type=None, model=None, properties=None, items=None,
|
|
schema_format=None, required=None, default=None, nullable=False,
|
|
enum=None, deprecated=False, all_of=None):
|
|
self.type = schema_type and SchemaType(schema_type)
|
|
self.model = model
|
|
self.properties = properties and dict(properties) or {}
|
|
self.items = items
|
|
self.format = SchemaFormat(schema_format)
|
|
self.required = required or []
|
|
self.default = default
|
|
self.nullable = nullable
|
|
self.enum = enum
|
|
self.deprecated = deprecated
|
|
self.all_of = all_of and list(all_of) or []
|
|
|
|
def __getitem__(self, name):
|
|
return self.properties[name]
|
|
|
|
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
|
|
|
|
def get_all_required_properties(self):
|
|
required = self.required.copy()
|
|
|
|
for subschema in self.all_of:
|
|
subschema_req = subschema.get_all_required_properties()
|
|
required += subschema_req
|
|
|
|
return required
|
|
|
|
def get_cast_mapping(self):
|
|
mapping = DEFAULT_CAST_CALLABLE_GETTER.copy()
|
|
mapping.update({
|
|
SchemaType.ARRAY: self._unmarshal_collection,
|
|
SchemaType.OBJECT: self._unmarshal_object,
|
|
})
|
|
|
|
return defaultdict(lambda: lambda x: x, mapping)
|
|
|
|
def cast(self, value):
|
|
"""Cast value to schema type"""
|
|
if value is None:
|
|
if not self.nullable:
|
|
raise InvalidValueType("Null value for non-nullable schema")
|
|
return self.default
|
|
|
|
if self.type is None:
|
|
return value
|
|
|
|
cast_mapping = self.get_cast_mapping()
|
|
|
|
if self.type in cast_mapping and value == '':
|
|
return None
|
|
|
|
cast_callable = cast_mapping[self.type]
|
|
try:
|
|
return cast_callable(value)
|
|
except ValueError:
|
|
raise InvalidValueType(
|
|
"Failed to cast value of {0} to {1}".format(value, self.type)
|
|
)
|
|
|
|
def unmarshal(self, value):
|
|
"""Unmarshal parameter from the value."""
|
|
if self.deprecated:
|
|
warnings.warn(
|
|
"The schema is deprecated", DeprecationWarning)
|
|
casted = self.cast(value)
|
|
|
|
if casted is None and not self.required:
|
|
return None
|
|
|
|
if self.enum and casted not in self.enum:
|
|
raise InvalidValue(
|
|
"Value of {0} not in enum choices: {1}".format(
|
|
value, self.enum)
|
|
)
|
|
|
|
return casted
|
|
|
|
def _unmarshal_collection(self, value):
|
|
return list(map(self.items.unmarshal, value))
|
|
|
|
def _unmarshal_object(self, value):
|
|
if isinstance(value, (str, bytes)):
|
|
value = loads(value)
|
|
|
|
all_properties = self.get_all_properties()
|
|
all_required_properties = self.get_all_required_properties()
|
|
all_properties_keys = all_properties.keys()
|
|
value_keys = value.keys()
|
|
|
|
extra_props = set(value_keys) - set(all_properties_keys)
|
|
|
|
if extra_props:
|
|
raise UndefinedSchemaProperty(
|
|
"Undefined properties in schema: {0}".format(extra_props))
|
|
|
|
properties = {}
|
|
for prop_name, prop in iteritems(all_properties):
|
|
try:
|
|
prop_value = value[prop_name]
|
|
except KeyError:
|
|
if prop_name in all_required_properties:
|
|
raise MissingProperty(
|
|
"Missing schema property {0}".format(prop_name))
|
|
if not prop.nullable and not prop.default:
|
|
continue
|
|
prop_value = prop.default
|
|
properties[prop_name] = prop.unmarshal(prop_value)
|
|
return ModelFactory().create(properties, name=self.model)
|
|
|
|
|
|
class PropertiesGenerator(object):
|
|
|
|
def __init__(self, dereferencer):
|
|
self.dereferencer = dereferencer
|
|
|
|
def generate(self, properties):
|
|
for property_name, schema_spec in iteritems(properties):
|
|
schema = self._create_schema(schema_spec)
|
|
yield property_name, schema
|
|
|
|
def _create_schema(self, schema_spec):
|
|
return SchemaFactory(self.dereferencer).create(schema_spec)
|
|
|
|
|
|
class SchemaFactory(object):
|
|
|
|
def __init__(self, dereferencer):
|
|
self.dereferencer = dereferencer
|
|
|
|
def create(self, schema_spec):
|
|
schema_deref = self.dereferencer.dereference(schema_spec)
|
|
|
|
schema_type = schema_deref.get('type', 'object')
|
|
schema_format = schema_deref.get('format')
|
|
model = schema_deref.get('x-model', None)
|
|
required = schema_deref.get('required', False)
|
|
default = schema_deref.get('default', None)
|
|
properties_spec = schema_deref.get('properties', None)
|
|
items_spec = schema_deref.get('items', None)
|
|
nullable = schema_deref.get('nullable', False)
|
|
enum = schema_deref.get('enum', None)
|
|
deprecated = schema_deref.get('deprecated', False)
|
|
all_of_spec = schema_deref.get('allOf', None)
|
|
|
|
properties = None
|
|
if properties_spec:
|
|
properties = self.properties_generator.generate(properties_spec)
|
|
|
|
all_of = []
|
|
if all_of_spec:
|
|
all_of = map(self.create, all_of_spec)
|
|
|
|
items = None
|
|
if items_spec:
|
|
items = self._create_items(items_spec)
|
|
|
|
return Schema(
|
|
schema_type=schema_type, model=model, properties=properties,
|
|
items=items, schema_format=schema_format, required=required,
|
|
default=default, nullable=nullable, enum=enum,
|
|
deprecated=deprecated, all_of=all_of,
|
|
)
|
|
|
|
@property
|
|
@lru_cache()
|
|
def properties_generator(self):
|
|
return PropertiesGenerator(self.dereferencer)
|
|
|
|
def _create_items(self, items_spec):
|
|
return self.create(items_spec)
|
|
|
|
|
|
class SchemaRegistry(SchemaFactory):
|
|
|
|
def __init__(self, dereferencer):
|
|
super(SchemaRegistry, self).__init__(dereferencer)
|
|
self._schemas = {}
|
|
|
|
def get_or_create(self, schema_spec):
|
|
schema_deref = self.dereferencer.dereference(schema_spec)
|
|
model = schema_deref.get('x-model', None)
|
|
|
|
if model and model in self._schemas:
|
|
return self._schemas[model], False
|
|
|
|
return self.create(schema_deref), True
|
|
|
|
|
|
class SchemasGenerator(object):
|
|
|
|
def __init__(self, dereferencer, schemas_registry):
|
|
self.dereferencer = dereferencer
|
|
self.schemas_registry = schemas_registry
|
|
|
|
def generate(self, schemas_spec):
|
|
schemas_deref = self.dereferencer.dereference(schemas_spec)
|
|
|
|
for schema_name, schema_spec in iteritems(schemas_deref):
|
|
schema, _ = self.schemas_registry.get_or_create(schema_spec)
|
|
yield schema_name, schema
|