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
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
11 changes: 10 additions & 1 deletion openapi_core/schema/schemas/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion openapi_core/schema/schemas/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
41 changes: 40 additions & 1 deletion openapi_core/unmarshalling/schemas/unmarshallers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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()

Expand All @@ -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)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
74 changes: 74 additions & 0 deletions tests/unit/unmarshalling/test_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down