Skip to content

Commit 948c360

Browse files
committed
unmarshaller finders refactor
1 parent bc78dee commit 948c360

File tree

5 files changed

+187
-119
lines changed

5 files changed

+187
-119
lines changed

openapi_core/unmarshalling/schemas/unmarshallers.py

Lines changed: 128 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,69 @@ def __init__(
185185
self.unmarshallers_factory = unmarshallers_factory
186186
self.context = context
187187

188+
def _get_one_of_schema_unmarshaller(
189+
self,
190+
value: Any,
191+
type_override: Optional[str] = None,
192+
) -> Optional[BaseSchemaUnmarshaller]:
193+
if "oneOf" not in self.schema:
194+
return None
195+
196+
one_of_schemas = self.schema / "oneOf"
197+
for subschema in one_of_schemas:
198+
unmarshaller = self.unmarshallers_factory.create(
199+
subschema, type_override=type_override
200+
)
201+
try:
202+
unmarshaller.validate(value)
203+
except ValidateError:
204+
continue
205+
else:
206+
return unmarshaller
207+
return None
208+
209+
def _iter_any_of_schema_unmarshallers(
210+
self,
211+
value: Any,
212+
type_override: Optional[str] = None,
213+
) -> Iterator[BaseSchemaUnmarshaller]:
214+
if "anyOf" not in self.schema:
215+
return
216+
217+
any_of_schemas = self.schema / "anyOf"
218+
for subschema in any_of_schemas:
219+
unmarshaller = self.unmarshallers_factory.create(
220+
subschema, type_override=type_override
221+
)
222+
try:
223+
unmarshaller.validate(value)
224+
except ValidateError:
225+
continue
226+
else:
227+
yield unmarshaller
228+
229+
def _iter_all_of_schema_unmarshallers(
230+
self,
231+
value: Any,
232+
type_override: Optional[str] = None,
233+
) -> Iterator[BaseSchemaUnmarshaller]:
234+
if "allOf" not in self.schema:
235+
return
236+
237+
all_of_schemas = self.schema / "allOf"
238+
for subschema in all_of_schemas:
239+
if "type" not in subschema:
240+
continue
241+
unmarshaller = self.unmarshallers_factory.create(
242+
subschema, type_override=type_override
243+
)
244+
try:
245+
unmarshaller.validate(value)
246+
except ValidateError:
247+
continue
248+
else:
249+
yield unmarshaller
250+
188251

189252
class ArrayUnmarshaller(ComplexUnmarshaller):
190253

@@ -221,52 +284,50 @@ def unmarshal(self, value: Any) -> Any:
221284

222285
return object_class(**properties)
223286

224-
def format(self, value: Any) -> Any:
287+
def format(self, value: Any, schema_only: bool = False) -> Any:
225288
formatted = super().format(value)
226-
return self._unmarshal_properties(formatted)
289+
return self._unmarshal_properties(formatted, schema_only=schema_only)
290+
291+
def _unmarshal_properties(
292+
self, value: Any, schema_only: bool = False
293+
) -> Any:
294+
properties = {}
227295

228-
def _clone(self, schema: Spec) -> "ObjectUnmarshaller":
229-
return cast(
230-
"ObjectUnmarshaller",
231-
self.unmarshallers_factory.create(schema, "object"),
296+
one_of_unmarshaller = cast(
297+
Optional["ObjectUnmarshaller"],
298+
self._get_one_of_schema_unmarshaller(
299+
value, type_override="object"
300+
),
232301
)
302+
if one_of_unmarshaller is not None:
303+
one_of_properties = one_of_unmarshaller.format(
304+
value, schema_only=True
305+
)
306+
properties.update(one_of_properties)
233307

234-
def _unmarshal_properties(self, value: Any) -> Any:
235-
properties = {}
308+
any_of_unmarshallers = cast(
309+
Iterator["ObjectUnmarshaller"],
310+
self._iter_any_of_schema_unmarshallers(
311+
value, type_override="object"
312+
),
313+
)
314+
for any_of_unmarshaller in any_of_unmarshallers:
315+
any_of_properties = any_of_unmarshaller.format(
316+
value, schema_only=True
317+
)
318+
properties.update(any_of_properties)
236319

237-
if "oneOf" in self.schema:
238-
one_of_properties = None
239-
for one_of_schema in self.schema / "oneOf":
240-
try:
241-
unmarshalled = self._clone(one_of_schema).format(value)
242-
except (UnmarshalError, ValueError):
243-
pass
244-
else:
245-
if one_of_properties is not None:
246-
log.warning("multiple valid oneOf schemas found")
247-
continue
248-
one_of_properties = unmarshalled
249-
250-
if one_of_properties is None:
251-
log.warning("valid oneOf schema not found")
252-
else:
253-
properties.update(one_of_properties)
254-
255-
elif "anyOf" in self.schema:
256-
any_of_properties = None
257-
for any_of_schema in self.schema / "anyOf":
258-
try:
259-
unmarshalled = self._clone(any_of_schema).format(value)
260-
except (UnmarshalError, ValueError):
261-
pass
262-
else:
263-
any_of_properties = unmarshalled
264-
break
265-
266-
if any_of_properties is None:
267-
log.warning("valid anyOf schema not found")
268-
else:
269-
properties.update(any_of_properties)
320+
all_of_unmarshallers = cast(
321+
Iterator["ObjectUnmarshaller"],
322+
self._iter_all_of_schema_unmarshallers(
323+
value, type_override="object"
324+
),
325+
)
326+
for all_of_unmarshaller in all_of_unmarshallers:
327+
all_of_properties = all_of_unmarshaller.format(
328+
value, schema_only=True
329+
)
330+
properties.update(all_of_properties)
270331

271332
for prop_name, prop in get_all_properties(self.schema).items():
272333
read_only = prop.getkey("readOnly", False)
@@ -286,6 +347,9 @@ def _unmarshal_properties(self, value: Any) -> Any:
286347
prop_value
287348
)
288349

350+
if schema_only:
351+
return properties
352+
289353
additional_properties = self.schema.getkey(
290354
"additionalProperties", True
291355
)
@@ -359,63 +423,30 @@ def type(self) -> List[str]:
359423
return self.SCHEMA_TYPES_ORDER
360424

361425
def unmarshal(self, value: Any) -> Any:
362-
one_of_schema = self._get_one_of_schema(value)
363-
if one_of_schema:
364-
return self.unmarshallers_factory.create(one_of_schema)(value)
426+
one_of_schema_unmarshaller = self._get_one_of_schema_unmarshaller(
427+
value
428+
)
429+
if one_of_schema_unmarshaller:
430+
return one_of_schema_unmarshaller(value)
365431

366-
any_of_schema = self._get_any_of_schema(value)
367-
if any_of_schema:
368-
return self.unmarshallers_factory.create(any_of_schema)(value)
432+
any_of_schema_unmarshallers = self._iter_any_of_schema_unmarshallers(
433+
value
434+
)
435+
try:
436+
any_of_schema_unmarshaller = next(any_of_schema_unmarshallers)
437+
except StopIteration:
438+
pass
439+
else:
440+
return any_of_schema_unmarshaller(value)
369441

370-
all_of_schema = self._get_all_of_schema(value)
371-
if all_of_schema:
372-
return self.unmarshallers_factory.create(all_of_schema)(value)
442+
all_of_schema_unmarshallers = self._iter_all_of_schema_unmarshallers(
443+
value
444+
)
445+
try:
446+
all_of_schema_unmarshaller = next(all_of_schema_unmarshallers)
447+
except StopIteration:
448+
pass
449+
else:
450+
return all_of_schema_unmarshaller(value)
373451

374452
return super().unmarshal(value)
375-
376-
def _get_one_of_schema(self, value: Any) -> Optional[Spec]:
377-
if "oneOf" not in self.schema:
378-
return None
379-
380-
one_of_schemas = self.schema / "oneOf"
381-
for subschema in one_of_schemas:
382-
unmarshaller = self.unmarshallers_factory.create(subschema)
383-
try:
384-
unmarshaller.validate(value)
385-
except ValidateError:
386-
continue
387-
else:
388-
return subschema
389-
return None
390-
391-
def _get_any_of_schema(self, value: Any) -> Optional[Spec]:
392-
if "anyOf" not in self.schema:
393-
return None
394-
395-
any_of_schemas = self.schema / "anyOf"
396-
for subschema in any_of_schemas:
397-
unmarshaller = self.unmarshallers_factory.create(subschema)
398-
try:
399-
unmarshaller.validate(value)
400-
except ValidateError:
401-
continue
402-
else:
403-
return subschema
404-
return None
405-
406-
def _get_all_of_schema(self, value: Any) -> Optional[Spec]:
407-
if "allOf" not in self.schema:
408-
return None
409-
410-
all_of_schemas = self.schema / "allOf"
411-
for subschema in all_of_schemas:
412-
if "type" not in subschema:
413-
continue
414-
unmarshaller = self.unmarshallers_factory.create(subschema)
415-
try:
416-
unmarshaller.validate(value)
417-
except ValidateError:
418-
continue
419-
else:
420-
return subschema
421-
return None

tests/integration/contrib/django/data/v3.0/djangoproject/pets/views.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,13 @@ def post(self, request):
3939
"api-key": "12345",
4040
}
4141
assert request.openapi.body.__class__.__name__ == "PetCreate"
42-
assert request.openapi.body.name == "Cat"
43-
assert request.openapi.body.ears.__class__.__name__ == "Ears"
44-
assert request.openapi.body.ears.healthy is True
42+
assert request.openapi.body.name in ["Cat", "Bird"]
43+
if request.openapi.body.name == "Cat":
44+
assert request.openapi.body.ears.__class__.__name__ == "Ears"
45+
assert request.openapi.body.ears.healthy is True
46+
if request.openapi.body.name == "Bird":
47+
assert request.openapi.body.wings.__class__.__name__ == "Wings"
48+
assert request.openapi.body.wings.healthy is True
4549

4650
django_response = HttpResponse(status=201)
4751
django_response["X-Rate-Limit"] = "12"

tests/integration/contrib/django/test_django_project.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -225,16 +225,28 @@ def test_post_required_cookie_param_missing(self, client):
225225
assert response.status_code == 400
226226
assert response.json() == expected_data
227227

228-
def test_post_valid(self, client):
228+
@pytest.mark.parametrize(
229+
"data_json",
230+
[
231+
{
232+
"id": 12,
233+
"name": "Cat",
234+
"ears": {
235+
"healthy": True,
236+
},
237+
},
238+
{
239+
"id": 12,
240+
"name": "Bird",
241+
"wings": {
242+
"healthy": True,
243+
},
244+
},
245+
],
246+
)
247+
def test_post_valid(self, client, data_json):
229248
client.cookies.load({"user": 1})
230249
content_type = "application/json"
231-
data_json = {
232-
"id": 12,
233-
"name": "Cat",
234-
"ears": {
235-
"healthy": True,
236-
},
237-
}
238250
headers = {
239251
"HTTP_AUTHORIZATION": "Basic testuser",
240252
"HTTP_HOST": "staging.gigantic-server.com",

tests/integration/contrib/falcon/data/v3.0/falconproject/pets/resources.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,18 @@ def on_post(self, request, response):
3838
"api-key": "12345",
3939
}
4040
assert request.context.openapi.body.__class__.__name__ == "PetCreate"
41-
assert request.context.openapi.body.name == "Cat"
42-
assert request.context.openapi.body.ears.__class__.__name__ == "Ears"
43-
assert request.context.openapi.body.ears.healthy is True
41+
assert request.context.openapi.body.name in ["Cat", "Bird"]
42+
if request.context.openapi.body.name == "Cat":
43+
assert (
44+
request.context.openapi.body.ears.__class__.__name__ == "Ears"
45+
)
46+
assert request.context.openapi.body.ears.healthy is True
47+
if request.context.openapi.body.name == "Bird":
48+
assert (
49+
request.context.openapi.body.wings.__class__.__name__
50+
== "Wings"
51+
)
52+
assert request.context.openapi.body.wings.healthy is True
4453

4554
response.status = HTTP_201
4655
response.set_header("X-Rate-Limit", "12")

tests/integration/contrib/falcon/test_falcon_project.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -210,16 +210,28 @@ def test_post_required_cookie_param_missing(self, client):
210210
assert response.status_code == 400
211211
assert response.json == expected_data
212212

213-
def test_post_valid(self, client):
213+
@pytest.mark.parametrize(
214+
"data_json",
215+
[
216+
{
217+
"id": 12,
218+
"name": "Cat",
219+
"ears": {
220+
"healthy": True,
221+
},
222+
},
223+
{
224+
"id": 12,
225+
"name": "Bird",
226+
"wings": {
227+
"healthy": True,
228+
},
229+
},
230+
],
231+
)
232+
def test_post_valid(self, client, data_json):
214233
cookies = {"user": 1}
215234
content_type = "application/json"
216-
data_json = {
217-
"id": 12,
218-
"name": "Cat",
219-
"ears": {
220-
"healthy": True,
221-
},
222-
}
223235
headers = {
224236
"Authorization": "Basic testuser",
225237
"Api-Key": self.api_key_encoded,

0 commit comments

Comments
 (0)