Skip to content

Commit 1d9d271

Browse files
committed
Media type encoding support
1 parent 5a02484 commit 1d9d271

File tree

5 files changed

+171
-30
lines changed

5 files changed

+171
-30
lines changed
+92-10
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,113 @@
11
import warnings
22
from typing import Any
3+
from typing import Dict
34
from typing import Optional
45
from xml.etree.ElementTree import ParseError
56

67
from openapi_core.deserializing.media_types.datatypes import (
78
DeserializerCallable,
89
)
10+
from openapi_core.deserializing.media_types.datatypes import (
11+
MediaTypeDeserializersDict,
12+
)
913
from openapi_core.deserializing.media_types.exceptions import (
1014
MediaTypeDeserializeError,
1115
)
16+
from openapi_core.schema.encodings import get_encoding_default_content_type
17+
from openapi_core.spec import Spec
1218

1319

14-
class CallableMediaTypeDeserializer:
20+
class ContentTypesDeserializer:
1521
def __init__(
1622
self,
17-
mimetype: str,
18-
deserializer_callable: Optional[DeserializerCallable] = None,
23+
media_type_deserializers: Optional[MediaTypeDeserializersDict] = None,
24+
extra_media_type_deserializers: Optional[MediaTypeDeserializersDict] = None,
1925
):
20-
self.mimetype = mimetype
21-
self.deserializer_callable = deserializer_callable
26+
if media_type_deserializers is None:
27+
media_type_deserializers = {}
28+
self.media_type_deserializers = media_type_deserializers
29+
if extra_media_type_deserializers is None:
30+
extra_media_type_deserializers = {}
31+
self.extra_media_type_deserializers = extra_media_type_deserializers
2232

23-
def deserialize(self, value: Any) -> Any:
24-
if self.deserializer_callable is None:
25-
warnings.warn(f"Unsupported {self.mimetype} mimetype")
33+
def deserialize(self, mimetype: str, value: Any) -> Any:
34+
deserializer_callable = self.get_deserializer_callable(mimetype)
35+
if deserializer_callable is None:
36+
warnings.warn(f"Unsupported {mimetype} mimetype")
2637
return value
2738

2839
try:
29-
return self.deserializer_callable(value)
40+
return deserializer_callable(value)
3041
except (ParseError, ValueError, TypeError, AttributeError):
31-
raise MediaTypeDeserializeError(self.mimetype, value)
42+
raise MediaTypeDeserializeError(mimetype, value)
43+
44+
def get_deserializer_callable(
45+
self,
46+
mimetype: str,
47+
) -> Optional[DeserializerCallable]:
48+
if mimetype in self.extra_media_type_deserializers:
49+
return self.extra_media_type_deserializers[mimetype]
50+
return self.media_type_deserializers.get(mimetype)
51+
52+
53+
class MediaTypeDeserializer(ContentTypesDeserializer):
54+
def __init__(
55+
self,
56+
schema: Spec,
57+
mimetype: str,
58+
content_types_deserializers: ContentTypesDeserializer,
59+
encoding: Optional[Spec] = None,
60+
):
61+
self.schema = schema
62+
self.mimetype = mimetype
63+
self.content_types_deserializers = content_types_deserializers
64+
self.encoding = encoding
65+
66+
def deserialize(self, value: Any) -> Any:
67+
deserialized = self.content_types_deserializers.deserialize(self.mimetype, value)
68+
69+
if self.mimetype != "application/x-www-form-urlencoded" and not self.mimetype.startswith("multipart"):
70+
return deserialized
71+
72+
return self.decode(deserialized)
73+
74+
def evolve(self, schema: Spec, mimetype: str) -> "MediaTypeDeserializer":
75+
cls = self.__class__
76+
77+
return cls(
78+
schema,
79+
mimetype,
80+
self.content_types_deserializers,
81+
)
82+
83+
def decode(self, value: Dict[str, Any]) -> Dict[str, Any]:
84+
return {
85+
prop_name: self.decode_property(prop_name, prop_value)
86+
for prop_name, prop_value in value.items()
87+
}
88+
89+
def decode_property(self, prop_name: str, value: Any) -> Any:
90+
schema_props = self.schema.get("properties")
91+
prop_schema = None
92+
if schema_props is not None and prop_name in schema_props:
93+
prop_schema = self.schema.get(prop_name)
94+
prop_content_type = self.get_property_content_type(prop_name, prop_schema)
95+
prop_deserializer = self.evolve(
96+
schema=prop_schema,
97+
mimetype=prop_content_type,
98+
)
99+
return prop_deserializer.deserialize(value)
100+
101+
def get_property_content_type(self, prop_name: str, prop_schema: Optional[Spec] = None) -> str:
102+
if self.encoding is None:
103+
return get_encoding_default_content_type(prop_schema)
104+
105+
if prop_name not in self.encoding:
106+
return get_encoding_default_content_type(prop_schema)
107+
108+
prep_encoding = self.encoding.get(prop_name)
109+
prop_content_type = prep_encoding.getkey("contentType")
110+
if prop_content_type is None:
111+
return get_encoding_default_content_type(prop_schema)
112+
113+
return prop_content_type

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

+17-15
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77
MediaTypeDeserializersDict,
88
)
99
from openapi_core.deserializing.media_types.deserializers import (
10-
CallableMediaTypeDeserializer,
10+
ContentTypesDeserializer,
1111
)
12+
from openapi_core.deserializing.media_types.deserializers import (
13+
MediaTypeDeserializer,
14+
)
15+
from openapi_core.spec import Spec
1216

1317

1418
class MediaTypeDeserializersFactory:
@@ -22,25 +26,23 @@ def __init__(
2226

2327
def create(
2428
self,
29+
schema: Spec,
2530
mimetype: str,
31+
encoding: Optional[Spec] = None,
2632
extra_media_type_deserializers: Optional[
2733
MediaTypeDeserializersDict
2834
] = None,
29-
) -> CallableMediaTypeDeserializer:
35+
) -> MediaTypeDeserializer:
3036
if extra_media_type_deserializers is None:
3137
extra_media_type_deserializers = {}
32-
deserialize_callable = self.get_deserializer_callable(
33-
mimetype,
34-
extra_media_type_deserializers=extra_media_type_deserializers,
35-
)
3638

37-
return CallableMediaTypeDeserializer(mimetype, deserialize_callable)
39+
content_types_deserializer = ContentTypesDeserializer(
40+
self.media_type_deserializers,
41+
extra_media_type_deserializers,
42+
)
3843

39-
def get_deserializer_callable(
40-
self,
41-
mimetype: str,
42-
extra_media_type_deserializers: MediaTypeDeserializersDict,
43-
) -> Optional[DeserializerCallable]:
44-
if mimetype in extra_media_type_deserializers:
45-
return extra_media_type_deserializers[mimetype]
46-
return self.media_type_deserializers.get(mimetype)
44+
return MediaTypeDeserializer(
45+
schema, mimetype,
46+
content_types_deserializer,
47+
encoding=encoding,
48+
)

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 openapi_core.spec import Spec
4+
5+
6+
def get_encoding_default_content_type(prop_schema: Optional[Spec]) -> str:
7+
if prop_schema is None:
8+
return "text/plain"
9+
10+
prop_type = prop_schema.getkey("type")
11+
if prop_type is None:
12+
return "text/plain"
13+
14+
prop_format = prop_schema.getkey("format")
15+
if prop_type == "string" and prop_format in ["binary", "base64"]:
16+
return "application/octet-stream"
17+
18+
if prop_type == "object":
19+
return "application/json"
20+
21+
if prop_type == "array":
22+
prop_items = prop_schema.get("items")
23+
prop_items_type = prop_items.getkey("type")
24+
if prop_items_type == "object":
25+
return "application/json"
26+
27+
return "text/plain"

Diff for: openapi_core/validation/validators.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,16 @@ def _get_media_type(self, content: Spec, mimetype: str) -> MediaType:
7878
finder = MediaTypeFinder(content)
7979
return finder.find(mimetype)
8080

81-
def _deserialise_data(self, mimetype: str, value: Any) -> Any:
81+
def _deserialise_data(self, media_type: Spec, mimetype: str, value: Any) -> Any:
82+
schema = media_type.get("schema")
83+
encoding = None
84+
if "encoding" in media_type:
85+
encoding = media_type.get("encoding")
86+
8287
deserializer = self.media_type_deserializers_factory.create(
88+
schema,
8389
mimetype,
90+
encoding=encoding,
8491
extra_media_type_deserializers=self.extra_media_type_deserializers,
8592
)
8693
return deserializer.deserialize(value)
@@ -152,7 +159,7 @@ def _get_param_or_header_value_and_schema(
152159
else:
153160
content = param_or_header / "content"
154161
mimetype, media_type = next(content.items())
155-
deserialised = self._deserialise_data(mimetype, raw_value)
162+
deserialised = self._deserialise_data(media_type, mimetype, raw_value)
156163
schema = media_type / "schema"
157164
casted = self._cast(schema, deserialised)
158165
return casted, schema
@@ -161,7 +168,7 @@ def _get_content_value_and_schema(
161168
self, raw: Any, mimetype: str, content: Spec
162169
) -> Tuple[Any, Optional[Spec]]:
163170
media_type, mimetype = self._get_media_type(content, mimetype)
164-
deserialised = self._deserialise_data(mimetype, raw)
171+
deserialised = self._deserialise_data(media_type, mimetype, raw)
165172
casted = self._cast(media_type, deserialised)
166173

167174
if "schema" not in media_type:

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

+25-2
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,19 @@ class TestMediaTypeDeserializer:
1414
def deserializer_factory(self):
1515
def create_deserializer(
1616
media_type,
17+
schema=None,
18+
encoding=None,
1719
media_type_deserializers=media_type_deserializers,
1820
extra_media_type_deserializers=None,
1921
):
22+
if schema is None:
23+
schema or {}
2024
return MediaTypeDeserializersFactory(
2125
media_type_deserializers,
2226
).create(
27+
schema,
2328
media_type,
29+
encoding=encoding,
2430
extra_media_type_deserializers=extra_media_type_deserializers,
2531
)
2632

@@ -132,7 +138,15 @@ def test_urlencoded_form_empty(self, deserializer_factory):
132138

133139
def test_urlencoded_form_simple(self, deserializer_factory):
134140
mimetype = "application/x-www-form-urlencoded"
135-
deserializer = deserializer_factory(mimetype)
141+
schema = {
142+
"type": "object",
143+
"properties": {
144+
"param1": {
145+
"type": "string",
146+
},
147+
},
148+
}
149+
deserializer = deserializer_factory(mimetype, schema=schema)
136150
value = "param1=test"
137151

138152
result = deserializer.deserialize(value)
@@ -150,7 +164,16 @@ def test_data_form_empty(self, deserializer_factory, value):
150164

151165
def test_data_form_simple(self, deserializer_factory):
152166
mimetype = "multipart/form-data"
153-
deserializer = deserializer_factory(mimetype)
167+
schema = {
168+
"type": "object",
169+
"properties": {
170+
"param1": {
171+
"type": "string",
172+
"format": "binary",
173+
},
174+
},
175+
}
176+
deserializer = deserializer_factory(mimetype, schema=schema)
154177
value = (
155178
b'Content-Type: multipart/form-data; boundary="'
156179
b'===============2872712225071193122=="\n'

0 commit comments

Comments
 (0)