Skip to content

Commit dbd7da7

Browse files
committed
Media type encoding support
1 parent df1f1e1 commit dbd7da7

File tree

5 files changed

+198
-31
lines changed

5 files changed

+198
-31
lines changed
+112-10
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,135 @@
11
import warnings
22
from typing import Any
3+
from typing import Dict
34
from typing import Optional
5+
from typing import cast
46
from xml.etree.ElementTree import ParseError
57

8+
from jsonschema_path import SchemaPath
9+
610
from openapi_core.deserializing.media_types.datatypes import (
711
DeserializerCallable,
812
)
13+
from openapi_core.deserializing.media_types.datatypes import (
14+
MediaTypeDeserializersDict,
15+
)
916
from openapi_core.deserializing.media_types.exceptions import (
1017
MediaTypeDeserializeError,
1118
)
19+
from openapi_core.schema.encodings import get_encoding_default_content_type
20+
21+
22+
class ContentTypesDeserializer:
23+
def __init__(
24+
self,
25+
media_type_deserializers: Optional[MediaTypeDeserializersDict] = None,
26+
extra_media_type_deserializers: Optional[
27+
MediaTypeDeserializersDict
28+
] = None,
29+
):
30+
if media_type_deserializers is None:
31+
media_type_deserializers = {}
32+
self.media_type_deserializers = media_type_deserializers
33+
if extra_media_type_deserializers is None:
34+
extra_media_type_deserializers = {}
35+
self.extra_media_type_deserializers = extra_media_type_deserializers
36+
37+
def deserialize(self, mimetype: str, value: Any, **parameters: str) -> Any:
38+
deserializer_callable = self.get_deserializer_callable(mimetype)
39+
if deserializer_callable is None:
40+
warnings.warn(f"Unsupported {mimetype} mimetype")
41+
return value
42+
43+
try:
44+
return deserializer_callable(value, **parameters)
45+
except (ParseError, ValueError, TypeError, AttributeError):
46+
raise MediaTypeDeserializeError(mimetype, value)
47+
48+
def get_deserializer_callable(
49+
self,
50+
mimetype: str,
51+
) -> Optional[DeserializerCallable]:
52+
if mimetype in self.extra_media_type_deserializers:
53+
return self.extra_media_type_deserializers[mimetype]
54+
return self.media_type_deserializers.get(mimetype)
1255

1356

14-
class CallableMediaTypeDeserializer:
57+
class MediaTypeDeserializer:
1558
def __init__(
1659
self,
1760
mimetype: str,
18-
deserializer_callable: Optional[DeserializerCallable] = None,
61+
content_types_deserializers: ContentTypesDeserializer,
62+
schema: Optional[SchemaPath] = None,
63+
encoding: Optional[SchemaPath] = None,
1964
**parameters: str,
2065
):
66+
self.schema = schema
2167
self.mimetype = mimetype
22-
self.deserializer_callable = deserializer_callable
68+
self.content_types_deserializers = content_types_deserializers
69+
self.encoding = encoding
2370
self.parameters = parameters
2471

2572
def deserialize(self, value: Any) -> Any:
26-
if self.deserializer_callable is None:
27-
warnings.warn(f"Unsupported {self.mimetype} mimetype")
28-
return value
73+
deserialized = self.content_types_deserializers.deserialize(
74+
self.mimetype, value, **self.parameters
75+
)
2976

30-
try:
31-
return self.deserializer_callable(value, **self.parameters)
32-
except (ParseError, ValueError, TypeError, AttributeError):
33-
raise MediaTypeDeserializeError(self.mimetype, value)
77+
if (
78+
self.mimetype != "application/x-www-form-urlencoded"
79+
and not self.mimetype.startswith("multipart")
80+
):
81+
return deserialized
82+
83+
return self.decode(deserialized)
84+
85+
def evolve(
86+
self, mimetype: str, schema: Optional[SchemaPath]
87+
) -> "MediaTypeDeserializer":
88+
cls = self.__class__
89+
90+
return cls(
91+
mimetype,
92+
self.content_types_deserializers,
93+
schema=schema,
94+
)
95+
96+
def decode(self, value: Dict[str, Any]) -> Dict[str, Any]:
97+
return {
98+
prop_name: self.decode_property(prop_name, prop_value)
99+
for prop_name, prop_value in value.items()
100+
}
101+
102+
def decode_property(self, prop_name: str, value: Any) -> Any:
103+
# schema is required for multipart
104+
assert self.schema
105+
schema_props = self.schema.get("properties")
106+
prop_schema = None
107+
if schema_props is not None and prop_name in schema_props:
108+
prop_schema = cast(
109+
Optional[SchemaPath],
110+
schema_props.get(prop_name),
111+
)
112+
prop_content_type = self.get_property_content_type(
113+
prop_name, prop_schema
114+
)
115+
prop_deserializer = self.evolve(
116+
prop_content_type,
117+
prop_schema,
118+
)
119+
return prop_deserializer.deserialize(value)
120+
121+
def get_property_content_type(
122+
self, prop_name: str, prop_schema: Optional[SchemaPath] = None
123+
) -> str:
124+
if self.encoding is None:
125+
return get_encoding_default_content_type(prop_schema)
126+
127+
if prop_name not in self.encoding:
128+
return get_encoding_default_content_type(prop_schema)
129+
130+
prep_encoding = self.encoding.get(prop_name)
131+
prop_content_type = prep_encoding.getkey("contentType")
132+
if prop_content_type is None:
133+
return get_encoding_default_content_type(prop_schema)
134+
135+
return cast(str, prop_content_type)

Diff for: openapi_core/deserializing/media_types/factories.py

+18-16
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
from typing import Mapping
22
from typing import Optional
33

4+
from jsonschema_path import SchemaPath
5+
46
from openapi_core.deserializing.media_types.datatypes import (
57
DeserializerCallable,
68
)
79
from openapi_core.deserializing.media_types.datatypes import (
810
MediaTypeDeserializersDict,
911
)
1012
from openapi_core.deserializing.media_types.deserializers import (
11-
CallableMediaTypeDeserializer,
13+
ContentTypesDeserializer,
14+
)
15+
from openapi_core.deserializing.media_types.deserializers import (
16+
MediaTypeDeserializer,
1217
)
1318

1419

@@ -24,29 +29,26 @@ def __init__(
2429
def create(
2530
self,
2631
mimetype: str,
32+
schema: Optional[SchemaPath] = None,
2733
parameters: Optional[Mapping[str, str]] = None,
34+
encoding: Optional[SchemaPath] = None,
2835
extra_media_type_deserializers: Optional[
2936
MediaTypeDeserializersDict
3037
] = None,
31-
) -> CallableMediaTypeDeserializer:
38+
) -> MediaTypeDeserializer:
3239
if parameters is None:
3340
parameters = {}
3441
if extra_media_type_deserializers is None:
3542
extra_media_type_deserializers = {}
36-
deserialize_callable = self.get_deserializer_callable(
37-
mimetype,
38-
extra_media_type_deserializers=extra_media_type_deserializers,
43+
content_types_deserializer = ContentTypesDeserializer(
44+
self.media_type_deserializers,
45+
extra_media_type_deserializers,
3946
)
4047

41-
return CallableMediaTypeDeserializer(
42-
mimetype, deserialize_callable, **parameters
48+
return MediaTypeDeserializer(
49+
mimetype,
50+
content_types_deserializer,
51+
schema=schema,
52+
encoding=encoding,
53+
**parameters,
4354
)
44-
45-
def get_deserializer_callable(
46-
self,
47-
mimetype: str,
48-
extra_media_type_deserializers: MediaTypeDeserializersDict,
49-
) -> Optional[DeserializerCallable]:
50-
if mimetype in extra_media_type_deserializers:
51-
return extra_media_type_deserializers[mimetype]
52-
return self.media_type_deserializers.get(mimetype)

Diff for: openapi_core/schema/encodings.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from typing import Optional
2+
3+
from jsonschema_path import SchemaPath
4+
5+
6+
def get_encoding_default_content_type(
7+
prop_schema: Optional[SchemaPath],
8+
) -> str:
9+
if prop_schema is None:
10+
return "text/plain"
11+
12+
prop_type = prop_schema.getkey("type")
13+
if prop_type is None:
14+
return "text/plain"
15+
16+
prop_format = prop_schema.getkey("format")
17+
if prop_type == "string" and prop_format in ["binary", "base64"]:
18+
return "application/octet-stream"
19+
20+
if prop_type == "object":
21+
return "application/json"
22+
23+
if prop_type == "array":
24+
prop_items = prop_schema / "items"
25+
return get_encoding_default_content_type(prop_items)
26+
27+
return "text/plain"

Diff for: openapi_core/validation/validators.py

+15-3
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,22 @@ def _find_media_type(
9999
return finder.find(mimetype)
100100

101101
def _deserialise_media_type(
102-
self, mimetype: str, parameters: Mapping[str, str], value: Any
102+
self,
103+
media_type: SchemaPath,
104+
mimetype: str,
105+
parameters: Mapping[str, str],
106+
value: Any,
103107
) -> Any:
108+
schema = media_type.get("schema")
109+
encoding = None
110+
if "encoding" in media_type:
111+
encoding = media_type.get("encoding")
104112
deserializer = self.media_type_deserializers_factory.create(
105113
mimetype,
106-
extra_media_type_deserializers=self.extra_media_type_deserializers,
114+
schema=schema,
107115
parameters=parameters,
116+
encoding=encoding,
117+
extra_media_type_deserializers=self.extra_media_type_deserializers,
108118
)
109119
return deserializer.deserialize(value)
110120

@@ -214,7 +224,9 @@ def _convert_content_schema_value_and_schema(
214224
mime_type, parameters, media_type = self._find_media_type(
215225
content, mimetype
216226
)
217-
deserialised = self._deserialise_media_type(mime_type, parameters, raw)
227+
deserialised = self._deserialise_media_type(
228+
media_type, mime_type, parameters, raw
229+
)
218230
casted = self._cast(media_type, deserialised)
219231

220232
if "schema" not in media_type:

Diff for: tests/unit/deserializing/test_media_types_deserializers.py

+26-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from xml.etree.ElementTree import Element
22

33
import pytest
4+
from jsonschema_path import SchemaPath
45

56
from openapi_core.deserializing.exceptions import DeserializeError
67
from openapi_core.deserializing.media_types import media_type_deserializers
@@ -14,6 +15,8 @@ class TestMediaTypeDeserializer:
1415
def deserializer_factory(self):
1516
def create_deserializer(
1617
media_type,
18+
schema=None,
19+
encoding=None,
1720
parameters=None,
1821
media_type_deserializers=media_type_deserializers,
1922
extra_media_type_deserializers=None,
@@ -22,7 +25,9 @@ def create_deserializer(
2225
media_type_deserializers,
2326
).create(
2427
media_type,
28+
schema=schema,
2529
parameters=parameters,
30+
encoding=encoding,
2631
extra_media_type_deserializers=extra_media_type_deserializers,
2732
)
2833

@@ -148,7 +153,16 @@ def test_urlencoded_form_empty(self, deserializer_factory):
148153

149154
def test_urlencoded_form_simple(self, deserializer_factory):
150155
mimetype = "application/x-www-form-urlencoded"
151-
deserializer = deserializer_factory(mimetype)
156+
schema_dict = {
157+
"type": "object",
158+
"properties": {
159+
"param1": {
160+
"type": "string",
161+
},
162+
},
163+
}
164+
schema = SchemaPath.from_dict(schema_dict)
165+
deserializer = deserializer_factory(mimetype, schema=schema)
152166
value = "param1=test"
153167

154168
result = deserializer.deserialize(value)
@@ -166,7 +180,17 @@ def test_data_form_empty(self, deserializer_factory, value):
166180

167181
def test_data_form_simple(self, deserializer_factory):
168182
mimetype = "multipart/form-data"
169-
deserializer = deserializer_factory(mimetype)
183+
schema_dict = {
184+
"type": "object",
185+
"properties": {
186+
"param1": {
187+
"type": "string",
188+
"format": "binary",
189+
},
190+
},
191+
}
192+
schema = SchemaPath.from_dict(schema_dict)
193+
deserializer = deserializer_factory(mimetype, schema=schema)
170194
value = (
171195
b'Content-Type: multipart/form-data; boundary="'
172196
b'===============2872712225071193122=="\n'

0 commit comments

Comments
 (0)