Skip to content

WIP Add any-of #288

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

Closed
wants to merge 1 commit into from
Closed
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
11 changes: 10 additions & 1 deletion openapi_core/schema/schemas/factories.py
Original file line number Diff line number Diff line change
@@ -31,6 +31,7 @@ def create(self, schema_spec):
deprecated = schema_deref.get('deprecated', False)
all_of_spec = schema_deref.get('allOf', None)
one_of_spec = schema_deref.get('oneOf', None)
any_of_spec = schema_deref.get('anyOf', None)
additional_properties_spec = schema_deref.get('additionalProperties',
True)
min_items = schema_deref.get('minItems', None)
@@ -63,6 +64,10 @@ def create(self, schema_spec):
if one_of_spec:
one_of = list(map(self.create, one_of_spec))

any_of = []
if any_of_spec:
any_of = list(map(self.create, any_of_spec))

items = None
if items_spec:
items = self._create_items(items_spec)
@@ -75,7 +80,7 @@ def create(self, schema_spec):
schema_type=schema_type, properties=properties,
items=items, schema_format=schema_format, required=required,
default=default, nullable=nullable, enum=enum,
deprecated=deprecated, all_of=all_of, one_of=one_of,
deprecated=deprecated, all_of=all_of, one_of=one_of, any_of=any_of,
additional_properties=additional_properties,
min_items=min_items, max_items=max_items, min_length=min_length,
max_length=max_length, pattern=pattern, unique_items=unique_items,
@@ -118,6 +123,10 @@ class SchemaDictFactory(object):
'one_of',
dest_prop_name='oneOf', is_list=True, dest_default=[],
),
Contribution(
'any_of',
dest_prop_name='anyOf', is_list=True, dest_default=[],
),
Contribution(
'additional_properties',
dest_prop_name='additionalProperties', dest_default=True,
3 changes: 2 additions & 1 deletion openapi_core/schema/schemas/models.py
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@ class Schema(object):
def __init__(
self, schema_type=None, properties=None, items=None,
schema_format=None, required=None, default=NoValue, nullable=False,
enum=None, deprecated=False, all_of=None, one_of=None,
enum=None, deprecated=False, all_of=None, one_of=None, any_of=None,
additional_properties=True, min_items=None, max_items=None,
min_length=None, max_length=None, pattern=None, unique_items=False,
minimum=None, maximum=None, multiple_of=None,
@@ -40,6 +40,7 @@ def __init__(
self.deprecated = deprecated
self.all_of = all_of and list(all_of) or []
self.one_of = one_of and list(one_of) or []
self.any_of = any_of and list(any_of) or []
self.additional_properties = additional_properties
self.min_items = int(min_items) if min_items is not None else None
self.max_items = int(max_items) if max_items is not None else None
41 changes: 40 additions & 1 deletion openapi_core/unmarshalling/schemas/unmarshallers.py
Original file line number Diff line number Diff line change
@@ -187,6 +187,23 @@ def _unmarshal_object(self, value=NoValue):
if properties is None:
log.warning("valid oneOf schema not found")

if self.schema.any_of:
properties = None
for any_of_schema in self.schema.any_of:
try:
unmarshalled = self._unmarshal_properties(
value, any_of_schema)
except (UnmarshalError, ValueError):
pass
else:
if properties is not None:
log.warning("multiple valid anyOf schemas found")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's allowed to have many valid schemas for anyof and properties need to be added rather than replaced

continue
properties = unmarshalled

if properties is None:
log.warning("valid anyOf schema not found")

else:
properties = self._unmarshal_properties(value)

@@ -196,7 +213,8 @@ def _unmarshal_object(self, value=NoValue):

return properties

def _unmarshal_properties(self, value=NoValue, one_of_schema=None):
def _unmarshal_properties(self, value=NoValue, one_of_schema=None,
any_of_schema=None):
all_props = self.schema.get_all_properties()
all_props_names = self.schema.get_all_properties_names()

@@ -205,6 +223,11 @@ def _unmarshal_properties(self, value=NoValue, one_of_schema=None):
all_props_names |= one_of_schema.\
get_all_properties_names()

if any_of_schema is not None:
all_props.update(any_of_schema.get_all_properties())
all_props_names |= any_of_schema.\
get_all_properties_names()

value_props_names = value.keys()
extra_props = set(value_props_names) - set(all_props_names)

@@ -253,6 +276,10 @@ def __call__(self, value=NoValue):
if one_of_schema:
return self.unmarshallers_factory.create(one_of_schema)(value)

any_of_schema = self._get_any_of_schema(value)
if any_of_schema:
return self.unmarshallers_factory.create(any_of_schema)(value)

all_of_schema = self._get_all_of_schema(value)
if all_of_schema:
return self.unmarshallers_factory.create(all_of_schema)(value)
@@ -283,6 +310,18 @@ def _get_one_of_schema(self, value):
else:
return subschema

def _get_any_of_schema(self, value):
if not self.schema.any_of:
return
for subschema in self.schema.any_of:
unmarshaller = self.unmarshallers_factory.create(subschema)
try:
unmarshaller.validate(value)
except ValidateError:
continue
else:
return subschema

def _get_all_of_schema(self, value):
if not self.schema.all_of:
return
74 changes: 74 additions & 0 deletions tests/unit/unmarshalling/test_validate.py
Original file line number Diff line number Diff line change
@@ -529,6 +529,80 @@ def test_unambiguous_one_of(self, value, validator_factory):

assert result is None

@pytest.mark.parametrize('value', [Model(), ])
def test_object_multiple_any_of(self, value, validator_factory):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple valid schemas are allowed for anyOf. It shouldn't raise error.

any_of = [
Schema('object'), Schema('object'),
]
schema = Schema('object', any_of=any_of)

with pytest.raises(InvalidSchemaValue):
validator_factory(schema).validate(value)

@pytest.mark.parametrize('value', [{}, ])
def test_object_different_type_any_of(self, value, validator_factory):
any_of = [
Schema('integer'), Schema('string'),
]
schema = Schema('object', any_of=any_of)

with pytest.raises(InvalidSchemaValue):
validator_factory(schema).validate(value)

@pytest.mark.parametrize('value', [{}, ])
def test_object_no_any_of(self, value, validator_factory):
any_of = [
Schema(
'object',
properties={'test1': Schema('string')},
required=['test1', ],
),
Schema(
'object',
properties={'test2': Schema('string')},
required=['test2', ],
),
]
schema = Schema('object', any_of=any_of)

with pytest.raises(InvalidSchemaValue):
validator_factory(schema).validate(value)

@pytest.mark.parametrize('value', [
{
'foo': u("FOO"),
},
{
'foo': u("FOO"),
'bar': u("BAR"),
},
])
def test_unambiguous_any_of(self, value, validator_factory):
any_of = [
Schema(
'object',
properties={
'foo': Schema('string'),
},
additional_properties=False,
required=['foo'],
),
Schema(
'object',
properties={
'foo': Schema('string'),
'bar': Schema('string'),
},
additional_properties=False,
required=['foo', 'bar'],
),
]
schema = Schema('object', any_of=any_of)

result = validator_factory(schema).validate(value)

assert result is None

@pytest.mark.parametrize('value', [{}, ])
def test_object_default_property(self, value, validator_factory):
schema = Schema('object', default='value1')