Skip to content

Commit b05e846

Browse files
committed
unmarshaller finders refactor
1 parent bc78dee commit b05e846

File tree

5 files changed

+161
-114
lines changed

5 files changed

+161
-114
lines changed

openapi_core/unmarshalling/schemas/unmarshallers.py

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

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

189240
class ArrayUnmarshaller(ComplexUnmarshaller):
190241

@@ -221,52 +272,41 @@ def unmarshal(self, value: Any) -> Any:
221272

222273
return object_class(**properties)
223274

224-
def format(self, value: Any) -> Any:
275+
def format(self, value: Any, schema_only: bool = False) -> Any:
225276
formatted = super().format(value)
226-
return self._unmarshal_properties(formatted)
277+
return self._unmarshal_properties(formatted, schema_only=schema_only)
227278

228279
def _clone(self, schema: Spec) -> "ObjectUnmarshaller":
229280
return cast(
230281
"ObjectUnmarshaller",
231282
self.unmarshallers_factory.create(schema, "object"),
232283
)
233284

234-
def _unmarshal_properties(self, value: Any) -> Any:
285+
def _unmarshal_properties(
286+
self, value: Any, schema_only: bool = False
287+
) -> Any:
235288
properties = {}
236289

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)
290+
one_of_unmarshaller = self._get_one_of_schema_unmarshaller(value)
291+
if one_of_unmarshaller is not None:
292+
one_of_properties = one_of_unmarshaller.format(
293+
value, schema_only=True
294+
)
295+
properties.update(one_of_properties)
296+
297+
any_of_unmarshallers = self._iter_any_of_schema_unmarshallers(value)
298+
for any_of_unmarshaller in any_of_unmarshallers:
299+
any_of_properties = any_of_unmarshaller.format(
300+
value, schema_only=True
301+
)
302+
properties.update(any_of_properties)
303+
304+
all_of_unmarshallers = self._iter_all_of_schema_unmarshallers(value)
305+
for all_of_unmarshaller in all_of_unmarshallers:
306+
all_of_properties = all_of_unmarshaller.format(
307+
value, schema_only=True
308+
)
309+
properties.update(all_of_properties)
270310

271311
for prop_name, prop in get_all_properties(self.schema).items():
272312
read_only = prop.getkey("readOnly", False)
@@ -286,6 +326,9 @@ def _unmarshal_properties(self, value: Any) -> Any:
286326
prop_value
287327
)
288328

329+
if schema_only:
330+
return properties
331+
289332
additional_properties = self.schema.getkey(
290333
"additionalProperties", True
291334
)
@@ -359,63 +402,30 @@ def type(self) -> List[str]:
359402
return self.SCHEMA_TYPES_ORDER
360403

361404
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)
405+
one_of_schema_unmarshaller = self._get_one_of_schema_unmarshaller(
406+
value
407+
)
408+
if one_of_schema_unmarshaller:
409+
return one_of_schema_unmarshaller(value)
365410

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)
411+
any_of_schema_unmarshallers = self._iter_any_of_schema_unmarshallers(
412+
value
413+
)
414+
try:
415+
any_of_schema_unmarshaller = next(any_of_schema_unmarshallers)
416+
except StopIteration:
417+
pass
418+
else:
419+
return any_of_schema_unmarshaller(value)
369420

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)
421+
all_of_schema_unmarshallers = self._iter_all_of_schema_unmarshallers(
422+
value
423+
)
424+
try:
425+
all_of_schema_unmarshaller = next(all_of_schema_unmarshallers)
426+
except StopIteration:
427+
pass
428+
else:
429+
return all_of_schema_unmarshaller(value)
373430

374431
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)