Skip to content

OneOf schema support #40

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions openapi_core/schema/schemas/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,11 @@ class UndefinedSchemaProperty(OpenAPISchemaError):

class MissingSchemaProperty(OpenAPISchemaError):
pass


class NoOneOfSchema(OpenAPISchemaError):
pass


class MultipleOneOfSchema(OpenAPISchemaError):
pass
7 changes: 6 additions & 1 deletion openapi_core/schema/schemas/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def create(self, schema_spec):
enum = schema_deref.get('enum', None)
deprecated = schema_deref.get('deprecated', False)
all_of_spec = schema_deref.get('allOf', None)
one_of_spec = schema_deref.get('oneOf', None)

properties = None
if properties_spec:
Expand All @@ -36,6 +37,10 @@ def create(self, schema_spec):
if 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
if items_spec:
items = self._create_items(items_spec)
Expand All @@ -44,7 +49,7 @@ def create(self, schema_spec):
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,
deprecated=deprecated, all_of=all_of, one_of=one_of,
)

@property
Expand Down
77 changes: 67 additions & 10 deletions openapi_core/schema/schemas/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from openapi_core.schema.schemas.enums import SchemaType, SchemaFormat
from openapi_core.schema.schemas.exceptions import (
InvalidSchemaValue, UndefinedSchemaProperty, MissingSchemaProperty,
OpenAPISchemaError, NoOneOfSchema, MultipleOneOfSchema,
)
from openapi_core.schema.schemas.util import forcebool

Expand All @@ -27,7 +28,7 @@ class Schema(object):
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):
enum=None, deprecated=False, all_of=None, one_of=None):
self.type = schema_type and SchemaType(schema_type)
self.model = model
self.properties = properties and dict(properties) or {}
Expand All @@ -39,6 +40,10 @@ def __init__(
self.enum = enum
self.deprecated = deprecated
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):
return self.properties[name]
Expand All @@ -52,14 +57,35 @@ def get_all_properties(self):

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):
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()

for subschema in self.all_of:
subschema_req = subschema.get_all_required_properties()
required += subschema_req

return required
return set(required)

def get_cast_mapping(self):
mapping = self.DEFAULT_CAST_CALLABLE_GETTER.copy()
Expand Down Expand Up @@ -119,27 +145,58 @@ def _unmarshal_object(self, value):
raise InvalidSchemaValue(
"Value of {0} not an object".format(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()
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)

return ModelFactory().create(properties, name=self.model)

extra_props = set(value_keys) - set(all_properties_keys)
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:
raise UndefinedSchemaProperty(
"Undefined properties in schema: {0}".format(extra_props))

properties = {}
for prop_name, prop in iteritems(all_properties):
for prop_name, prop in iteritems(all_props):
try:
prop_value = value[prop_name]
except KeyError:
if prop_name in all_required_properties:
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
properties[prop_name] = prop.unmarshal(prop_value)
return ModelFactory().create(properties, name=self.model)
return properties
58 changes: 58 additions & 0 deletions tests/integration/data/v3.0/petstore.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,22 @@ paths:
$ref: "#/components/schemas/TagList"
default:
$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:
schemas:
Address:
Expand Down Expand Up @@ -163,6 +179,9 @@ components:
allOf:
- $ref: "#/components/schemas/PetCreatePartOne"
- $ref: "#/components/schemas/PetCreatePartTwo"
oneOf:
- $ref: "#/components/schemas/Cat"
- $ref: "#/components/schemas/Bird"
PetCreatePartOne:
type: object
x-model: PetCreatePartOne
Expand All @@ -183,6 +202,38 @@ components:
$ref: "#/components/schemas/Position"
healthy:
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:
type: array
items:
Expand All @@ -201,6 +252,13 @@ components:
properties:
data:
$ref: "#/components/schemas/Pet"
TagCreate:
type: object
required:
- name
properties:
name:
type: string
TagList:
type: array
items:
Expand Down
Loading