Skip to content

Commit 5f5e1d6

Browse files
committed
Validation context
1 parent 4e70267 commit 5f5e1d6

11 files changed

Lines changed: 914 additions & 27 deletions

File tree

openapi_core/deserializing/media_types/deserializers.py

Lines changed: 79 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from openapi_core.schema.protocols import SuportsGetList
2525
from openapi_core.schema.schemas import get_properties
2626
from openapi_core.validation.schemas.validators import SchemaValidator
27+
from openapi_core.validation.schemas.exceptions import ValidateError
2728

2829
if TYPE_CHECKING:
2930
from openapi_core.casting.schemas.casters import SchemaCaster
@@ -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
@@ -253,3 +250,76 @@ def decode_property_content_type(
253250
return list(map(prop_deserializer.deserialize, value))
254251

255252
return prop_deserializer.deserialize(location[prop_name])
253+
254+
def get_composed_one_of_schema(
255+
self, location: Mapping[str, Any]
256+
) -> Optional[SchemaPath]:
257+
assert self.schema_validator is not None
258+
259+
if not self.mimetype.startswith("multipart"):
260+
return self.schema_validator.get_one_of_schema(
261+
location, caster=self.schema_caster
262+
)
263+
264+
if self.schema is None or "oneOf" not in self.schema:
265+
return None
266+
267+
for subschema in self.schema / "oneOf":
268+
if self.is_decoded_subschema_valid(subschema, location):
269+
return subschema
270+
271+
return None
272+
273+
def iter_composed_any_of_schemas(
274+
self, location: Mapping[str, Any]
275+
) -> list[SchemaPath]:
276+
assert self.schema_validator is not None
277+
278+
if not self.mimetype.startswith("multipart"):
279+
return list(
280+
self.schema_validator.iter_any_of_schemas(
281+
location, caster=self.schema_caster
282+
)
283+
)
284+
285+
if self.schema is None or "anyOf" not in self.schema:
286+
return []
287+
288+
return [
289+
subschema
290+
for subschema in self.schema / "anyOf"
291+
if self.is_decoded_subschema_valid(subschema, location)
292+
]
293+
294+
def iter_composed_all_of_schemas(
295+
self, location: Mapping[str, Any]
296+
) -> list[SchemaPath]:
297+
assert self.schema_validator is not None
298+
299+
if not self.mimetype.startswith("multipart"):
300+
return list(self.schema_validator.iter_all_of_schemas(location))
301+
302+
if self.schema is None or "allOf" not in self.schema:
303+
return []
304+
305+
return [
306+
subschema
307+
for subschema in self.schema / "allOf"
308+
if self.is_decoded_subschema_valid(subschema, location)
309+
]
310+
311+
def is_decoded_subschema_valid(
312+
self,
313+
subschema: SchemaPath,
314+
location: Mapping[str, Any],
315+
) -> bool:
316+
assert self.schema_validator is not None
317+
318+
deserializer = self.evolve(subschema)
319+
candidate = deserializer.decode(location)
320+
validator = self.schema_validator.evolve(subschema)
321+
try:
322+
validator.validate(candidate)
323+
except ValidateError:
324+
return False
325+
return True

openapi_core/deserializing/styles/util.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,19 @@ def split(value: str, separator: str = ",", step: int = 1) -> List[str]:
2222
return result
2323

2424

25+
def decode_form_value(value: Any) -> Any:
26+
if isinstance(value, bytes):
27+
try:
28+
return value.decode("utf-8")
29+
except UnicodeDecodeError:
30+
return value.decode("ASCII", errors="surrogateescape")
31+
32+
if isinstance(value, list):
33+
return [decode_form_value(item) for item in value]
34+
35+
return value
36+
37+
2538
def delimited_loads(
2639
explode: bool,
2740
name: str,
@@ -118,18 +131,19 @@ def form_loads(
118131
explode_type = (explode, schema_type)
119132
# color=blue,black,brown
120133
if explode_type == (False, "array"):
121-
return split(location[name], separator=",")
134+
value = decode_form_value(location[name])
135+
return split(value, separator=",")
122136
# color=blue&color=black&color=brown
123137
elif explode_type == (True, "array"):
124138
if name not in location:
125139
raise KeyError(name)
126140
if isinstance(location, SuportsGetAll):
127-
return location.getall(name)
141+
return decode_form_value(location.getall(name))
128142
if isinstance(location, SuportsGetList):
129-
return location.getlist(name)
130-
return location[name]
143+
return decode_form_value(location.getlist(name))
144+
return decode_form_value(location[name])
131145

132-
value = location[name]
146+
value = decode_form_value(location[name])
133147
# color=R,100,G,200,B,150
134148
if explode_type == (False, "object"):
135149
return dict(map(split, split(value, separator=",", step=2)))

openapi_core/unmarshalling/schemas/unmarshallers.py

Lines changed: 156 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from openapi_core.unmarshalling.schemas.datatypes import (
1616
FormatUnmarshallersDict,
1717
)
18+
from openapi_core.validation.schemas.datatypes import ValidationState
1819
from openapi_core.validation.schemas.validators import SchemaValidator
1920

2021
log = logging.getLogger(__name__)
@@ -39,6 +40,14 @@ class ArrayUnmarshaller(PrimitiveUnmarshaller):
3940
def __call__(self, value: Any) -> Optional[List[Any]]:
4041
return list(map(self.items_unmarshaller.unmarshal, value))
4142

43+
def unmarshal_state(self, state: ValidationState) -> Optional[List[Any]]:
44+
if state.item_states:
45+
return [
46+
self.items_unmarshaller.unmarshal_state(item_state)
47+
for item_state in state.item_states
48+
]
49+
return self(value=state.value)
50+
4251
@property
4352
def items_unmarshaller(self) -> "SchemaUnmarshaller":
4453
# sometimes we don't have any schema i.e. free-form objects
@@ -55,6 +64,14 @@ def __call__(self, value: Any) -> Any:
5564

5665
return object_class(**properties)
5766

67+
def unmarshal_state(self, state: ValidationState) -> Any:
68+
properties = self._unmarshal_properties_from_state(state)
69+
70+
fields: Iterable[str] = properties and properties.keys() or []
71+
object_class = self.object_class_factory.create(self.schema, fields)
72+
73+
return object_class(**properties)
74+
5875
@property
5976
def object_class_factory(self) -> ModelPathFactory:
6077
return ModelPathFactory()
@@ -125,6 +142,94 @@ def _unmarshal_properties(
125142
for prop_name, prop_value in value.items():
126143
if prop_name in properties:
127144
continue
145+
child_state = state.additional_property_states.get(prop_name)
146+
if child_state is not None:
147+
properties[prop_name] = (
148+
additional_prop_unmarshaler.unmarshal_state(child_state)
149+
)
150+
continue
151+
properties[prop_name] = additional_prop_unmarshaler.unmarshal(
152+
prop_value
153+
)
154+
155+
return properties
156+
157+
def _unmarshal_properties_from_state(
158+
self,
159+
state: ValidationState,
160+
schema_only: bool = False,
161+
) -> Any:
162+
value = state.value
163+
properties = {}
164+
165+
if state.one_of_state is not None:
166+
one_of_properties = self.evolve(
167+
state.one_of_state.schema
168+
)._unmarshal_properties_from_state(
169+
state.one_of_state,
170+
schema_only=True,
171+
)
172+
properties.update(one_of_properties)
173+
174+
for any_of_state in state.any_of_states:
175+
any_of_properties = self.evolve(
176+
any_of_state.schema
177+
)._unmarshal_properties_from_state(
178+
any_of_state,
179+
schema_only=True,
180+
)
181+
properties.update(any_of_properties)
182+
183+
for all_of_state in state.all_of_states:
184+
all_of_properties = self.evolve(
185+
all_of_state.schema
186+
)._unmarshal_properties_from_state(
187+
all_of_state,
188+
schema_only=True,
189+
)
190+
properties.update(all_of_properties)
191+
192+
for prop_name, prop_schema in get_properties(self.schema).items():
193+
child_state = state.property_states.get(prop_name)
194+
if child_state is not None:
195+
properties[prop_name] = self.schema_unmarshaller.evolve(
196+
prop_schema
197+
).unmarshal_state(child_state)
198+
continue
199+
200+
try:
201+
prop_value = value[prop_name]
202+
except KeyError:
203+
if "default" not in prop_schema:
204+
continue
205+
prop_value = (prop_schema / "default").read_value()
206+
properties[prop_name] = self.schema_unmarshaller.evolve(
207+
prop_schema
208+
).unmarshal(prop_value)
209+
210+
if schema_only:
211+
return properties
212+
213+
additional_properties = self.schema.get("additionalProperties", True)
214+
if additional_properties is not False:
215+
if additional_properties is True:
216+
additional_prop_schema = SchemaPath.from_dict(
217+
{"nullable": True}
218+
)
219+
else:
220+
additional_prop_schema = self.schema / "additionalProperties"
221+
additional_prop_unmarshaler = self.schema_unmarshaller.evolve(
222+
additional_prop_schema
223+
)
224+
for prop_name, prop_value in value.items():
225+
if prop_name in properties:
226+
continue
227+
child_state = state.additional_property_states.get(prop_name)
228+
if child_state is not None:
229+
properties[prop_name] = (
230+
additional_prop_unmarshaler.unmarshal_state(child_state)
231+
)
232+
continue
128233
properties[prop_name] = additional_prop_unmarshaler.unmarshal(
129234
prop_value
130235
)
@@ -143,6 +248,17 @@ def __call__(self, value: Any) -> Any:
143248
)
144249
return unmarshaller(value)
145250

251+
def unmarshal_state(self, state: ValidationState) -> Any:
252+
primitive_type = state.primitive_type
253+
if primitive_type is None:
254+
return None
255+
unmarshaller = self.schema_unmarshaller.get_type_unmarshaller(
256+
primitive_type
257+
)
258+
if hasattr(unmarshaller, "unmarshal_state"):
259+
return unmarshaller.unmarshal_state(state)
260+
return unmarshaller(state.value)
261+
146262

147263
class AnyUnmarshaller(MultiTypeUnmarshaller):
148264
pass
@@ -239,7 +355,11 @@ def __init__(
239355
self.formats_unmarshaller = formats_unmarshaller
240356

241357
def unmarshal(self, value: Any) -> Any:
242-
self.schema_validator.validate(value)
358+
state = self.schema_validator.validate_state(value)
359+
return self.unmarshal_state(state)
360+
361+
def unmarshal_state(self, state: ValidationState) -> Any:
362+
value = state.value
243363

244364
# skip unmarshalling for nullable in OpenAPI 3.0
245365
if value is None and (self.schema / "nullable").read_bool(
@@ -249,11 +369,14 @@ def unmarshal(self, value: Any) -> Any:
249369

250370
schema_type = (self.schema / "type").read_str_or_list(None)
251371
type_unmarshaller = self.get_type_unmarshaller(schema_type)
252-
typed = type_unmarshaller(value)
372+
if hasattr(type_unmarshaller, "unmarshal_state"):
373+
typed = type_unmarshaller.unmarshal_state(state)
374+
else:
375+
typed = type_unmarshaller(value)
253376
# skip finding format for None
254377
if typed is None:
255378
return None
256-
schema_format = self.find_format(value)
379+
schema_format = self.find_format(value, state=state)
257380
if schema_format is None:
258381
return typed
259382
# ignore incompatible formats
@@ -300,7 +423,21 @@ def evolve(self, schema: SchemaPath) -> "SchemaUnmarshaller":
300423
self.formats_unmarshaller,
301424
)
302425

303-
def find_format(self, value: Any) -> Optional[str]:
426+
def find_format(
427+
self,
428+
value: Any,
429+
state: Optional[ValidationState] = None,
430+
) -> Optional[str]:
431+
if state is not None:
432+
for schema in self.iter_valid_schemas_from_state(state):
433+
schema_validator = self.schema_validator.evolve(schema)
434+
primitive_type = schema_validator.get_primitive_type(value)
435+
if primitive_type != "string":
436+
continue
437+
if "format" in schema:
438+
return (schema / "format").read_str()
439+
return None
440+
304441
for schema in self.schema_validator.iter_valid_schemas(value):
305442
schema_validator = self.schema_validator.evolve(schema)
306443
primitive_type = schema_validator.get_primitive_type(value)
@@ -309,3 +446,18 @@ def find_format(self, value: Any) -> Optional[str]:
309446
if "format" in schema:
310447
return (schema / "format").read_str()
311448
return None
449+
450+
def iter_valid_schemas_from_state(
451+
self,
452+
state: ValidationState,
453+
) -> Iterable[SchemaPath]:
454+
yield state.schema
455+
456+
if state.one_of_state is not None:
457+
yield state.one_of_state.schema
458+
459+
for any_of_state in state.any_of_states:
460+
yield any_of_state.schema
461+
462+
for all_of_state in state.all_of_states:
463+
yield all_of_state.schema

0 commit comments

Comments
 (0)