Skip to content

Commit 36694ab

Browse files
committed
Validation context
1 parent 9342ce6 commit 36694ab

10 files changed

Lines changed: 1102 additions & 21 deletions

File tree

openapi_core/deserializing/media_types/deserializers.py

Lines changed: 161 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from openapi_core.schema.protocols import SuportsGetAll
2424
from openapi_core.schema.protocols import SuportsGetList
2525
from openapi_core.schema.schemas import get_properties
26+
from openapi_core.validation.schemas.exceptions import ValidateError
2627
from openapi_core.validation.schemas.validators import SchemaValidator
2728

2829
if TYPE_CHECKING:
@@ -126,6 +127,8 @@ def evolve(
126127
schema=schema,
127128
schema_validator=schema_validator,
128129
schema_caster=schema_caster,
130+
encoding=self.encoding,
131+
**self.parameters,
129132
)
130133

131134
def decode(
@@ -137,27 +140,21 @@ def decode(
137140

138141
# For urlencoded/multipart, use caster for oneOf/anyOf detection if validator available
139142
if self.schema_validator is not None:
140-
one_of_schema = self.schema_validator.get_one_of_schema(
141-
location, caster=self.schema_caster
142-
)
143+
one_of_schema = self.get_composed_one_of_schema(location)
143144
if one_of_schema is not None:
144145
one_of_properties = self.evolve(one_of_schema).decode(
145146
location, schema_only=True
146147
)
147148
properties.update(one_of_properties)
148149

149-
any_of_schemas = self.schema_validator.iter_any_of_schemas(
150-
location, caster=self.schema_caster
151-
)
150+
any_of_schemas = self.iter_composed_any_of_schemas(location)
152151
for any_of_schema in any_of_schemas:
153152
any_of_properties = self.evolve(any_of_schema).decode(
154153
location, schema_only=True
155154
)
156155
properties.update(any_of_properties)
157156

158-
all_of_schemas = self.schema_validator.iter_all_of_schemas(
159-
location
160-
)
157+
all_of_schemas = self.iter_composed_all_of_schemas(location)
161158
for all_of_schema in all_of_schemas:
162159
all_of_properties = self.evolve(all_of_schema).decode(
163160
location, schema_only=True
@@ -220,6 +217,13 @@ def decode_property_style(
220217
location: Mapping[str, Any],
221218
prep_encoding: SchemaPath,
222219
) -> Any:
220+
if self.mimetype.startswith("multipart"):
221+
location = self.normalize_multipart_form_location(
222+
prop_name,
223+
prop_schema,
224+
location,
225+
)
226+
223227
prop_style, prop_explode = get_style_and_explode(
224228
prep_encoding, default_location="query"
225229
)
@@ -228,6 +232,81 @@ def decode_property_style(
228232
)
229233
return prop_deserializer.deserialize(location)
230234

235+
def normalize_multipart_form_location(
236+
self,
237+
prop_name: str,
238+
prop_schema: SchemaPath,
239+
location: Mapping[str, Any],
240+
) -> Mapping[str, Any]:
241+
if not self.should_decode_multipart_form_value(prop_schema):
242+
return location
243+
244+
if prop_name not in location:
245+
return location
246+
247+
normalized = dict(location)
248+
value = location[prop_name]
249+
250+
if isinstance(value, bytes):
251+
normalized[prop_name] = self.decode_multipart_form_value(value)
252+
return normalized
253+
254+
if isinstance(value, list):
255+
normalized[prop_name] = [
256+
(
257+
self.decode_multipart_form_value(item)
258+
if isinstance(item, bytes)
259+
else item
260+
)
261+
for item in value
262+
]
263+
return normalized
264+
265+
if isinstance(location, SuportsGetAll):
266+
values = location.getall(prop_name)
267+
if any(isinstance(item, bytes) for item in values):
268+
normalized[prop_name] = [
269+
(
270+
self.decode_multipart_form_value(item)
271+
if isinstance(item, bytes)
272+
else item
273+
)
274+
for item in values
275+
]
276+
return normalized
277+
278+
if isinstance(location, SuportsGetList):
279+
values = location.getlist(prop_name)
280+
if any(isinstance(item, bytes) for item in values):
281+
normalized[prop_name] = [
282+
(
283+
self.decode_multipart_form_value(item)
284+
if isinstance(item, bytes)
285+
else item
286+
)
287+
for item in values
288+
]
289+
290+
return normalized
291+
292+
def should_decode_multipart_form_value(
293+
self,
294+
prop_schema: SchemaPath,
295+
) -> bool:
296+
schema_type = (prop_schema / "type").read_str(None)
297+
schema_format = (prop_schema / "format").read_str(None)
298+
299+
if schema_type in ["integer", "number", "boolean"]:
300+
return True
301+
302+
return schema_type == "string" and schema_format != "binary"
303+
304+
def decode_multipart_form_value(self, value: bytes) -> str:
305+
try:
306+
return value.decode("utf-8")
307+
except UnicodeDecodeError:
308+
return value.decode("ASCII", errors="surrogateescape")
309+
231310
def decode_property_content_type(
232311
self,
233312
prop_name: str,
@@ -253,3 +332,76 @@ def decode_property_content_type(
253332
return list(map(prop_deserializer.deserialize, value))
254333

255334
return prop_deserializer.deserialize(location[prop_name])
335+
336+
def get_composed_one_of_schema(
337+
self, location: Mapping[str, Any]
338+
) -> Optional[SchemaPath]:
339+
assert self.schema_validator is not None
340+
341+
if not self.mimetype.startswith("multipart"):
342+
return self.schema_validator.get_one_of_schema(
343+
location, caster=self.schema_caster
344+
)
345+
346+
if self.schema is None or "oneOf" not in self.schema:
347+
return None
348+
349+
for subschema in self.schema / "oneOf":
350+
if self.is_decoded_subschema_valid(subschema, location):
351+
return subschema
352+
353+
return None
354+
355+
def iter_composed_any_of_schemas(
356+
self, location: Mapping[str, Any]
357+
) -> list[SchemaPath]:
358+
assert self.schema_validator is not None
359+
360+
if not self.mimetype.startswith("multipart"):
361+
return list(
362+
self.schema_validator.iter_any_of_schemas(
363+
location, caster=self.schema_caster
364+
)
365+
)
366+
367+
if self.schema is None or "anyOf" not in self.schema:
368+
return []
369+
370+
return [
371+
subschema
372+
for subschema in self.schema / "anyOf"
373+
if self.is_decoded_subschema_valid(subschema, location)
374+
]
375+
376+
def iter_composed_all_of_schemas(
377+
self, location: Mapping[str, Any]
378+
) -> list[SchemaPath]:
379+
assert self.schema_validator is not None
380+
381+
if not self.mimetype.startswith("multipart"):
382+
return list(self.schema_validator.iter_all_of_schemas(location))
383+
384+
if self.schema is None or "allOf" not in self.schema:
385+
return []
386+
387+
return [
388+
subschema
389+
for subschema in self.schema / "allOf"
390+
if self.is_decoded_subschema_valid(subschema, location)
391+
]
392+
393+
def is_decoded_subschema_valid(
394+
self,
395+
subschema: SchemaPath,
396+
location: Mapping[str, Any],
397+
) -> bool:
398+
assert self.schema_validator is not None
399+
400+
deserializer = self.evolve(subschema)
401+
candidate = deserializer.decode(location)
402+
validator = self.schema_validator.evolve(subschema)
403+
try:
404+
validator.validate(candidate)
405+
except ValidateError:
406+
return False
407+
return True

0 commit comments

Comments
 (0)