1
0
Fork 0
mirror of https://github.com/correl/openapi-core.git synced 2025-04-09 17:00:10 -09:00

Merge pull request from p1c2u/feature/one-of-schema-support

OneOf schema support
This commit is contained in:
A 2018-05-30 09:13:30 +01:00 committed by GitHub
commit 4669c4763a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 263 additions and 60 deletions

View file

@ -15,3 +15,11 @@ class UndefinedSchemaProperty(OpenAPISchemaError):
class MissingSchemaProperty(OpenAPISchemaError): class MissingSchemaProperty(OpenAPISchemaError):
pass pass
class NoOneOfSchema(OpenAPISchemaError):
pass
class MultipleOneOfSchema(OpenAPISchemaError):
pass

View file

@ -27,6 +27,7 @@ class SchemaFactory(object):
enum = schema_deref.get('enum', None) enum = schema_deref.get('enum', None)
deprecated = schema_deref.get('deprecated', False) deprecated = schema_deref.get('deprecated', False)
all_of_spec = schema_deref.get('allOf', None) all_of_spec = schema_deref.get('allOf', None)
one_of_spec = schema_deref.get('oneOf', None)
properties = None properties = None
if properties_spec: if properties_spec:
@ -36,6 +37,10 @@ class SchemaFactory(object):
if all_of_spec: if all_of_spec:
all_of = map(self.create, all_of_spec) all_of = map(self.create, all_of_spec)
one_of = []
if one_of_spec:
one_of = map(self.create, one_of_spec)
items = None items = None
if items_spec: if items_spec:
items = self._create_items(items_spec) items = self._create_items(items_spec)
@ -44,7 +49,7 @@ class SchemaFactory(object):
schema_type=schema_type, model=model, properties=properties, schema_type=schema_type, model=model, properties=properties,
items=items, schema_format=schema_format, required=required, items=items, schema_format=schema_format, required=required,
default=default, nullable=nullable, enum=enum, default=default, nullable=nullable, enum=enum,
deprecated=deprecated, all_of=all_of, deprecated=deprecated, all_of=all_of, one_of=one_of,
) )
@property @property

View file

@ -9,6 +9,7 @@ from openapi_core.extensions.models.factories import ModelFactory
from openapi_core.schema.schemas.enums import SchemaType, SchemaFormat from openapi_core.schema.schemas.enums import SchemaType, SchemaFormat
from openapi_core.schema.schemas.exceptions import ( from openapi_core.schema.schemas.exceptions import (
InvalidSchemaValue, UndefinedSchemaProperty, MissingSchemaProperty, InvalidSchemaValue, UndefinedSchemaProperty, MissingSchemaProperty,
OpenAPISchemaError, NoOneOfSchema, MultipleOneOfSchema,
) )
from openapi_core.schema.schemas.util import forcebool from openapi_core.schema.schemas.util import forcebool
@ -27,7 +28,7 @@ class Schema(object):
def __init__( def __init__(
self, schema_type=None, model=None, properties=None, items=None, self, schema_type=None, model=None, properties=None, items=None,
schema_format=None, required=None, default=None, nullable=False, schema_format=None, required=None, default=None, nullable=False,
enum=None, deprecated=False, all_of=None): enum=None, deprecated=False, all_of=None, one_of=None):
self.type = schema_type and SchemaType(schema_type) self.type = schema_type and SchemaType(schema_type)
self.model = model self.model = model
self.properties = properties and dict(properties) or {} self.properties = properties and dict(properties) or {}
@ -39,6 +40,10 @@ class Schema(object):
self.enum = enum self.enum = enum
self.deprecated = deprecated self.deprecated = deprecated
self.all_of = all_of and list(all_of) or [] self.all_of = all_of and list(all_of) or []
self.one_of = one_of and list(one_of) or []
self._all_required_properties_cache = None
self._all_optional_properties_cache = None
def __getitem__(self, name): def __getitem__(self, name):
return self.properties[name] return self.properties[name]
@ -52,14 +57,35 @@ class Schema(object):
return properties return properties
def get_all_properties_names(self):
all_properties = self.get_all_properties()
return set(all_properties.keys())
def get_all_required_properties(self): def get_all_required_properties(self):
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)
for prop_name, val in all_properties.items()
if prop_name in required
)
def get_all_required_properties_names(self):
required = self.required.copy() required = self.required.copy()
for subschema in self.all_of: for subschema in self.all_of:
subschema_req = subschema.get_all_required_properties() subschema_req = subschema.get_all_required_properties()
required += subschema_req required += subschema_req
return required return set(required)
def get_cast_mapping(self): def get_cast_mapping(self):
mapping = self.DEFAULT_CAST_CALLABLE_GETTER.copy() mapping = self.DEFAULT_CAST_CALLABLE_GETTER.copy()
@ -119,27 +145,58 @@ class Schema(object):
raise InvalidSchemaValue( raise InvalidSchemaValue(
"Value of {0} not an object".format(value)) "Value of {0} not an object".format(value))
all_properties = self.get_all_properties() if self.one_of:
all_required_properties = self.get_all_required_properties() properties = None
all_properties_keys = all_properties.keys() for one_of_schema in self.one_of:
value_keys = value.keys() 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
extra_props = set(value_keys) - set(all_properties_keys) if properties is None:
raise NoOneOfSchema(
"Exactly one valid schema should be valid, None found.")
else:
properties = self._unmarshal_properties(value)
return ModelFactory().create(properties, name=self.model)
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()
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: if extra_props:
raise UndefinedSchemaProperty( raise UndefinedSchemaProperty(
"Undefined properties in schema: {0}".format(extra_props)) "Undefined properties in schema: {0}".format(extra_props))
properties = {} properties = {}
for prop_name, prop in iteritems(all_properties): 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_required_properties: if prop_name in all_req_props_names:
raise MissingSchemaProperty( raise MissingSchemaProperty(
"Missing schema property {0}".format(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) properties[prop_name] = prop.unmarshal(prop_value)
return ModelFactory().create(properties, name=self.model) return properties

View file

@ -123,6 +123,22 @@ paths:
$ref: "#/components/schemas/TagList" $ref: "#/components/schemas/TagList"
default: default:
$ref: "#/components/responses/ErrorResponse" $ref: "#/components/responses/ErrorResponse"
post:
summary: Create new tag
operationId: createTag
tags:
- tags
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TagCreate'
responses:
'200':
description: Null response
default:
$ref: "#/components/responses/ErrorResponse"
components: components:
schemas: schemas:
Address: Address:
@ -163,6 +179,9 @@ components:
allOf: allOf:
- $ref: "#/components/schemas/PetCreatePartOne" - $ref: "#/components/schemas/PetCreatePartOne"
- $ref: "#/components/schemas/PetCreatePartTwo" - $ref: "#/components/schemas/PetCreatePartTwo"
oneOf:
- $ref: "#/components/schemas/Cat"
- $ref: "#/components/schemas/Bird"
PetCreatePartOne: PetCreatePartOne:
type: object type: object
x-model: PetCreatePartOne x-model: PetCreatePartOne
@ -183,6 +202,38 @@ components:
$ref: "#/components/schemas/Position" $ref: "#/components/schemas/Position"
healthy: healthy:
type: boolean type: boolean
Bird:
type: object
x-model: Bird
required:
- wings
properties:
wings:
$ref: "#/components/schemas/Wings"
Wings:
type: object
x-model: Wings
required:
- healthy
properties:
healthy:
type: boolean
Cat:
type: object
x-model: Cat
required:
- ears
properties:
ears:
$ref: "#/components/schemas/Ears"
Ears:
type: object
x-model: Ears
required:
- healthy
properties:
healthy:
type: boolean
Pets: Pets:
type: array type: array
items: items:
@ -201,6 +252,13 @@ components:
properties: properties:
data: data:
$ref: "#/components/schemas/Pet" $ref: "#/components/schemas/Pet"
TagCreate:
type: object
required:
- name
properties:
name:
type: string
TagList: TagList:
type: array type: array
items: items:

View file

@ -15,7 +15,7 @@ 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, UndefinedSchemaProperty, MissingSchemaProperty, NoOneOfSchema,
) )
from openapi_core.schema.schemas.models import Schema from openapi_core.schema.schemas.models import Schema
from openapi_core.schema.servers.exceptions import InvalidServer from openapi_core.schema.servers.exceptions import InvalidServer
@ -369,7 +369,7 @@ class TestPetstore(object):
assert body is None assert body is None
def test_post_pets(self, spec, spec_dict): def test_post_birds(self, spec, spec_dict):
host_url = 'http://petstore.swagger.io/v1' host_url = 'http://petstore.swagger.io/v1'
path_pattern = '/v1/pets' path_pattern = '/v1/pets'
pet_name = 'Cat' pet_name = 'Cat'
@ -386,6 +386,9 @@ class TestPetstore(object):
'city': pet_city, 'city': pet_city,
}, },
'healthy': pet_healthy, 'healthy': pet_healthy,
'wings': {
'healthy': pet_healthy,
}
} }
data = json.dumps(data_json) data = json.dumps(data_json)
@ -412,7 +415,53 @@ class TestPetstore(object):
assert body.address.city == pet_city assert body.address.city == pet_city
assert body.healthy == pet_healthy assert body.healthy == pet_healthy
def test_post_pets_boolean_string(self, spec, spec_dict): def test_post_cats(self, spec, spec_dict):
host_url = 'http://petstore.swagger.io/v1'
path_pattern = '/v1/pets'
pet_name = 'Cat'
pet_tag = 'cats'
pet_street = 'Piekna'
pet_city = 'Warsaw'
pet_healthy = False
data_json = {
'name': pet_name,
'tag': pet_tag,
'position': '2',
'address': {
'street': pet_street,
'city': pet_city,
},
'healthy': pet_healthy,
'ears': {
'healthy': pet_healthy,
}
}
data = json.dumps(data_json)
request = MockRequest(
host_url, 'POST', '/pets',
path_pattern=path_pattern, data=data,
)
parameters = request.get_parameters(spec)
assert parameters == {}
body = request.get_body(spec)
schemas = spec_dict['components']['schemas']
pet_model = schemas['PetCreate']['x-model']
address_model = schemas['Address']['x-model']
assert body.__class__.__name__ == pet_model
assert body.name == pet_name
assert body.tag == pet_tag
assert body.position == 2
assert body.address.__class__.__name__ == address_model
assert body.address.street == pet_street
assert body.address.city == pet_city
assert body.healthy == pet_healthy
def test_post_cats_boolean_string(self, spec, spec_dict):
host_url = 'http://petstore.swagger.io/v1' host_url = 'http://petstore.swagger.io/v1'
path_pattern = '/v1/pets' path_pattern = '/v1/pets'
pet_name = 'Cat' pet_name = 'Cat'
@ -429,6 +478,9 @@ class TestPetstore(object):
'city': pet_city, 'city': pet_city,
}, },
'healthy': pet_healthy, 'healthy': pet_healthy,
'ears': {
'healthy': pet_healthy,
}
} }
data = json.dumps(data_json) data = json.dumps(data_json)
@ -455,25 +507,7 @@ class TestPetstore(object):
assert body.address.city == pet_city assert body.address.city == pet_city
assert body.healthy is False assert body.healthy is False
def test_post_pets_empty_body(self, spec, spec_dict): def test_post_no_one_of_schema(self, spec, spec_dict):
host_url = 'http://petstore.swagger.io/v1'
path_pattern = '/v1/pets'
data_json = {}
data = json.dumps(data_json)
request = MockRequest(
host_url, 'POST', '/pets',
path_pattern=path_pattern, data=data,
)
parameters = request.get_parameters(spec)
assert parameters == {}
with pytest.raises(MissingSchemaProperty):
request.get_body(spec)
def test_post_pets_extra_body_properties(self, spec, spec_dict):
host_url = 'http://petstore.swagger.io/v1' host_url = 'http://petstore.swagger.io/v1'
path_pattern = '/v1/pets' path_pattern = '/v1/pets'
pet_name = 'Cat' pet_name = 'Cat'
@ -493,15 +527,19 @@ class TestPetstore(object):
assert parameters == {} assert parameters == {}
with pytest.raises(UndefinedSchemaProperty): with pytest.raises(NoOneOfSchema):
request.get_body(spec) request.get_body(spec)
def test_post_pets_only_required_body(self, spec, spec_dict): def test_post_cats_only_required_body(self, spec, spec_dict):
host_url = 'http://petstore.swagger.io/v1' host_url = 'http://petstore.swagger.io/v1'
path_pattern = '/v1/pets' path_pattern = '/v1/pets'
pet_name = 'Cat' pet_name = 'Cat'
pet_healthy = True
data_json = { data_json = {
'name': pet_name, 'name': pet_name,
'ears': {
'healthy': pet_healthy,
}
} }
data = json.dumps(data_json) data = json.dumps(data_json)
@ -523,31 +561,6 @@ class TestPetstore(object):
assert not hasattr(body, 'tag') assert not hasattr(body, 'tag')
assert not hasattr(body, 'address') assert not hasattr(body, 'address')
def test_get_pets_wrong_body_type(self, spec):
host_url = 'http://petstore.swagger.io/v1'
path_pattern = '/v1/pets'
pet_name = 'Cat'
pet_tag = 'cats'
pet_address = 'address text'
data_json = {
'name': pet_name,
'tag': pet_tag,
'address': pet_address,
}
data = json.dumps(data_json)
request = MockRequest(
host_url, 'POST', '/pets',
path_pattern=path_pattern, data=data,
)
parameters = request.get_parameters(spec)
assert parameters == {}
with pytest.raises(InvalidMediaTypeValue):
request.get_body(spec)
def test_post_pets_raises_invalid_mimetype(self, spec): def test_post_pets_raises_invalid_mimetype(self, spec):
host_url = 'http://petstore.swagger.io/v1' host_url = 'http://petstore.swagger.io/v1'
path_pattern = '/v1/pets' path_pattern = '/v1/pets'
@ -685,3 +698,62 @@ class TestPetstore(object):
assert response_result.errors == [] assert response_result.errors == []
assert response_result.data == data_json assert response_result.data == data_json
def test_post_tags_extra_body_properties(self, spec, spec_dict):
host_url = 'http://petstore.swagger.io/v1'
path_pattern = '/v1/tags'
pet_name = 'Dog'
alias = 'kitty'
data_json = {
'name': pet_name,
'alias': alias,
}
data = json.dumps(data_json)
request = MockRequest(
host_url, 'POST', '/tags',
path_pattern=path_pattern, data=data,
)
parameters = request.get_parameters(spec)
assert parameters == {}
with pytest.raises(UndefinedSchemaProperty):
request.get_body(spec)
def test_post_tags_empty_body(self, spec, spec_dict):
host_url = 'http://petstore.swagger.io/v1'
path_pattern = '/v1/tags'
data_json = {}
data = json.dumps(data_json)
request = MockRequest(
host_url, 'POST', '/tags',
path_pattern=path_pattern, data=data,
)
parameters = request.get_parameters(spec)
assert parameters == {}
with pytest.raises(MissingSchemaProperty):
request.get_body(spec)
def test_post_tags_wrong_property_type(self, spec):
host_url = 'http://petstore.swagger.io/v1'
path_pattern = '/v1/tags'
tag_name = 123
data = json.dumps(tag_name)
request = MockRequest(
host_url, 'POST', '/tags',
path_pattern=path_pattern, data=data,
)
parameters = request.get_parameters(spec)
assert parameters == {}
with pytest.raises(InvalidMediaTypeValue):
request.get_body(spec)

View file

@ -123,6 +123,9 @@ class TestRequestValidator(object):
'address': { 'address': {
'street': pet_street, 'street': pet_street,
'city': pet_city, 'city': pet_city,
},
'ears': {
'healthy': True,
} }
} }
data = json.dumps(data_json) data = json.dumps(data_json)