Skip to content

Commit e3ec0e4

Browse files
p1c2uallcapssisp
committed
Add any-of
Co-authored-by: Coen van der Kamp <coen@fourdigits.nl> Co-authored-by: Sigurd Spieckermann <sigurd.spieckermann@gmail.com>
1 parent e3da8d3 commit e3ec0e4

File tree

4 files changed

+266
-50
lines changed

4 files changed

+266
-50
lines changed

openapi_core/schema/schemas.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from typing import Any
22
from typing import Dict
3-
from typing import Set
43

54
from openapi_core.spec import Spec
65

@@ -17,8 +16,3 @@ def get_all_properties(schema: Spec) -> Dict[str, Any]:
1716
properties_dict.update(subschema_props)
1817

1918
return properties_dict
20-
21-
22-
def get_all_properties_names(schema: Spec) -> Set[str]:
23-
all_properties = get_all_properties(schema)
24-
return set(all_properties.keys())

openapi_core/unmarshalling/schemas/unmarshallers.py

Lines changed: 81 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
from openapi_core.extensions.models.factories import ModelClassImporter
2121
from openapi_core.schema.schemas import get_all_properties
22-
from openapi_core.schema.schemas import get_all_properties_names
2322
from openapi_core.spec import Spec
2423
from openapi_core.unmarshalling.schemas.datatypes import FormattersDict
2524
from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
@@ -201,6 +200,15 @@ def object_class_factory(self) -> ModelClassImporter:
201200
return ModelClassImporter()
202201

203202
def unmarshal(self, value: Any) -> Any:
203+
properties = self.unmarshal_raw(value)
204+
205+
model = self.schema.getkey("x-model")
206+
fields: Iterable[str] = properties and properties.keys() or []
207+
object_class = self.object_class_factory.create(fields, model=model)
208+
209+
return object_class(**properties)
210+
211+
def unmarshal_raw(self, value: Any) -> Any:
204212
try:
205213
value = self.formatter.unmarshal(value)
206214
except ValueError as exc:
@@ -209,65 +217,57 @@ def unmarshal(self, value: Any) -> Any:
209217
else:
210218
return self._unmarshal_object(value)
211219

220+
def _clone(self, schema: Spec) -> "ObjectUnmarshaller":
221+
return ObjectUnmarshaller(
222+
schema,
223+
self.validator,
224+
self.formatter,
225+
self.unmarshallers_factory,
226+
self.context,
227+
)
228+
212229
def _unmarshal_object(self, value: Any) -> Any:
230+
properties = {}
231+
213232
if "oneOf" in self.schema:
214-
properties = None
233+
one_of_properties = None
215234
for one_of_schema in self.schema / "oneOf":
216235
try:
217-
unmarshalled = self._unmarshal_properties(
218-
value, one_of_schema
236+
unmarshalled = self._clone(one_of_schema).unmarshal_raw(
237+
value
219238
)
220239
except (UnmarshalError, ValueError):
221240
pass
222241
else:
223-
if properties is not None:
242+
if one_of_properties is not None:
224243
log.warning("multiple valid oneOf schemas found")
225244
continue
226-
properties = unmarshalled
245+
one_of_properties = unmarshalled
227246

228-
if properties is None:
247+
if one_of_properties is None:
229248
log.warning("valid oneOf schema not found")
249+
else:
250+
properties.update(one_of_properties)
230251

231-
else:
232-
properties = self._unmarshal_properties(value)
233-
234-
model = self.schema.getkey("x-model")
235-
fields: Iterable[str] = properties and properties.keys() or []
236-
object_class = self.object_class_factory.create(fields, model=model)
237-
238-
return object_class(**properties)
239-
240-
def _unmarshal_properties(
241-
self, value: Any, one_of_schema: Optional[Spec] = None
242-
) -> Dict[str, Any]:
243-
all_props = get_all_properties(self.schema)
244-
all_props_names = get_all_properties_names(self.schema)
245-
246-
if one_of_schema is not None:
247-
all_props.update(get_all_properties(one_of_schema))
248-
all_props_names |= get_all_properties_names(one_of_schema)
249-
250-
value_props_names = list(value.keys())
251-
extra_props = set(value_props_names) - set(all_props_names)
252+
elif "anyOf" in self.schema:
253+
any_of_properties = None
254+
for any_of_schema in self.schema / "anyOf":
255+
try:
256+
unmarshalled = self._clone(any_of_schema).unmarshal_raw(
257+
value
258+
)
259+
except (UnmarshalError, ValueError):
260+
pass
261+
else:
262+
any_of_properties = unmarshalled
263+
break
252264

253-
properties: Dict[str, Any] = {}
254-
additional_properties = self.schema.getkey(
255-
"additionalProperties", True
256-
)
257-
if additional_properties is not False:
258-
# free-form object
259-
if additional_properties is True:
260-
additional_prop_schema = Spec.from_dict({})
261-
# defined schema
265+
if any_of_properties is None:
266+
log.warning("valid anyOf schema not found")
262267
else:
263-
additional_prop_schema = self.schema / "additionalProperties"
264-
for prop_name in extra_props:
265-
prop_value = value[prop_name]
266-
properties[prop_name] = self.unmarshallers_factory.create(
267-
additional_prop_schema
268-
)(prop_value)
268+
properties.update(any_of_properties)
269269

270-
for prop_name, prop in list(all_props.items()):
270+
for prop_name, prop in get_all_properties(self.schema).items():
271271
read_only = prop.getkey("readOnly", False)
272272
if self.context == UnmarshalContext.REQUEST and read_only:
273273
continue
@@ -285,6 +285,24 @@ def _unmarshal_properties(
285285
prop_value
286286
)
287287

288+
additional_properties = self.schema.getkey(
289+
"additionalProperties", True
290+
)
291+
if additional_properties is not False:
292+
# free-form object
293+
if additional_properties is True:
294+
additional_prop_schema = Spec.from_dict({})
295+
# defined schema
296+
else:
297+
additional_prop_schema = self.schema / "additionalProperties"
298+
additional_prop_unmarshaler = self.unmarshallers_factory.create(
299+
additional_prop_schema
300+
)
301+
for prop_name, prop_value in value.items():
302+
if prop_name in properties:
303+
continue
304+
properties[prop_name] = additional_prop_unmarshaler(prop_value)
305+
288306
return properties
289307

290308

@@ -304,6 +322,10 @@ def unmarshal(self, value: Any) -> Any:
304322
if one_of_schema:
305323
return self.unmarshallers_factory.create(one_of_schema)(value)
306324

325+
any_of_schema = self._get_any_of_schema(value)
326+
if any_of_schema:
327+
return self.unmarshallers_factory.create(any_of_schema)(value)
328+
307329
all_of_schema = self._get_all_of_schema(value)
308330
if all_of_schema:
309331
return self.unmarshallers_factory.create(all_of_schema)(value)
@@ -338,6 +360,21 @@ def _get_one_of_schema(self, value: Any) -> Optional[Spec]:
338360
return subschema
339361
return None
340362

363+
def _get_any_of_schema(self, value: Any) -> Optional[Spec]:
364+
if "anyOf" not in self.schema:
365+
return None
366+
367+
any_of_schemas = self.schema / "anyOf"
368+
for subschema in any_of_schemas:
369+
unmarshaller = self.unmarshallers_factory.create(subschema)
370+
try:
371+
unmarshaller.validate(value)
372+
except ValidateError:
373+
continue
374+
else:
375+
return subschema
376+
return None
377+
341378
def _get_all_of_schema(self, value: Any) -> Optional[Spec]:
342379
if "allOf" not in self.schema:
343380
return None

tests/unit/unmarshalling/test_unmarshal.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,65 @@ def test_schema_any_one_of(self, unmarshaller_factory):
560560
spec = Spec.from_dict(schema)
561561
assert unmarshaller_factory(spec)(["hello"]) == ["hello"]
562562

563+
def test_schema_any_any_of(self, unmarshaller_factory):
564+
schema = {
565+
"anyOf": [
566+
{
567+
"type": "string",
568+
},
569+
{
570+
"type": "array",
571+
"items": {
572+
"type": "string",
573+
},
574+
},
575+
],
576+
}
577+
spec = Spec.from_dict(schema)
578+
assert unmarshaller_factory(spec)(["hello"]) == ["hello"]
579+
580+
def test_schema_object_any_of(self, unmarshaller_factory):
581+
schema = {
582+
"type": "object",
583+
"anyOf": [
584+
{
585+
"type": "object",
586+
"required": ["someint"],
587+
"properties": {"someint": {"type": "integer"}},
588+
},
589+
{
590+
"type": "object",
591+
"required": ["somestr"],
592+
"properties": {"somestr": {"type": "string"}},
593+
},
594+
],
595+
}
596+
spec = Spec.from_dict(schema)
597+
result = unmarshaller_factory(spec)({"someint": 1})
598+
599+
assert is_dataclass(result)
600+
assert result.someint == 1
601+
602+
def test_schema_object_any_of_invalid(self, unmarshaller_factory):
603+
schema = {
604+
"type": "object",
605+
"anyOf": [
606+
{
607+
"type": "object",
608+
"required": ["someint"],
609+
"properties": {"someint": {"type": "integer"}},
610+
},
611+
{
612+
"type": "object",
613+
"required": ["somestr"],
614+
"properties": {"somestr": {"type": "string"}},
615+
},
616+
],
617+
}
618+
spec = Spec.from_dict(schema)
619+
with pytest.raises(UnmarshalError):
620+
unmarshaller_factory(spec)({"someint": "1"})
621+
563622
def test_schema_any_all_of(self, unmarshaller_factory):
564623
schema = {
565624
"allOf": [

tests/unit/unmarshalling/test_validate.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -863,6 +863,132 @@ def test_unambiguous_one_of(self, value, validator_factory):
863863

864864
assert result is None
865865

866+
@pytest.mark.parametrize(
867+
"value",
868+
[
869+
{},
870+
],
871+
)
872+
def test_object_multiple_any_of(self, value, validator_factory):
873+
any_of = [
874+
{
875+
"type": "object",
876+
},
877+
{
878+
"type": "object",
879+
},
880+
]
881+
schema = {
882+
"type": "object",
883+
"anyOf": any_of,
884+
}
885+
spec = Spec.from_dict(schema)
886+
887+
result = validator_factory(spec).validate(value)
888+
889+
assert result is None
890+
891+
@pytest.mark.parametrize(
892+
"value",
893+
[
894+
{},
895+
],
896+
)
897+
def test_object_different_type_any_of(self, value, validator_factory):
898+
any_of = [{"type": "integer"}, {"type": "string"}]
899+
schema = {
900+
"type": "object",
901+
"anyOf": any_of,
902+
}
903+
spec = Spec.from_dict(schema)
904+
905+
with pytest.raises(InvalidSchemaValue):
906+
validator_factory(spec).validate(value)
907+
908+
@pytest.mark.parametrize(
909+
"value",
910+
[
911+
{},
912+
],
913+
)
914+
def test_object_no_any_of(self, value, validator_factory):
915+
any_of = [
916+
{
917+
"type": "object",
918+
"required": ["test1"],
919+
"properties": {
920+
"test1": {
921+
"type": "string",
922+
},
923+
},
924+
},
925+
{
926+
"type": "object",
927+
"required": ["test2"],
928+
"properties": {
929+
"test2": {
930+
"type": "string",
931+
},
932+
},
933+
},
934+
]
935+
schema = {
936+
"type": "object",
937+
"anyOf": any_of,
938+
}
939+
spec = Spec.from_dict(schema)
940+
941+
with pytest.raises(InvalidSchemaValue):
942+
validator_factory(spec).validate(value)
943+
944+
@pytest.mark.parametrize(
945+
"value",
946+
[
947+
{
948+
"foo": "FOO",
949+
},
950+
{
951+
"foo": "FOO",
952+
"bar": "BAR",
953+
},
954+
],
955+
)
956+
def test_unambiguous_any_of(self, value, validator_factory):
957+
any_of = [
958+
{
959+
"type": "object",
960+
"required": ["foo"],
961+
"properties": {
962+
"foo": {
963+
"type": "string",
964+
},
965+
},
966+
"additionalProperties": False,
967+
},
968+
{
969+
"type": "object",
970+
"required": ["foo", "bar"],
971+
"properties": {
972+
"foo": {
973+
"type": "string",
974+
},
975+
"bar": {
976+
"type": "string",
977+
},
978+
},
979+
"additionalProperties": False,
980+
},
981+
]
982+
schema = {
983+
"type": "object",
984+
"anyOf": any_of,
985+
}
986+
spec = Spec.from_dict(schema)
987+
988+
result = validator_factory(spec).validate(value)
989+
990+
assert result is None
991+
866992
@pytest.mark.parametrize(
867993
"value",
868994
[

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy