Skip to content

Commit 330cb71

Browse files
authored
Merge pull request #694 from python-openapi/feature/style-deserializing-reimplementation
Style deserializing reimplementation
2 parents df1f1e1 + 0327c5a commit 330cb71

File tree

12 files changed

+785
-203
lines changed

12 files changed

+785
-203
lines changed

Diff for: openapi_core/deserializing/styles/__init__.py

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,27 @@
1+
from openapi_core.deserializing.styles.datatypes import StyleDeserializersDict
12
from openapi_core.deserializing.styles.factories import (
23
StyleDeserializersFactory,
34
)
5+
from openapi_core.deserializing.styles.util import deep_object_loads
6+
from openapi_core.deserializing.styles.util import form_loads
7+
from openapi_core.deserializing.styles.util import label_loads
8+
from openapi_core.deserializing.styles.util import matrix_loads
9+
from openapi_core.deserializing.styles.util import pipe_delimited_loads
10+
from openapi_core.deserializing.styles.util import simple_loads
11+
from openapi_core.deserializing.styles.util import space_delimited_loads
412

513
__all__ = ["style_deserializers_factory"]
614

7-
style_deserializers_factory = StyleDeserializersFactory()
15+
style_deserializers: StyleDeserializersDict = {
16+
"matrix": matrix_loads,
17+
"label": label_loads,
18+
"form": form_loads,
19+
"simple": simple_loads,
20+
"spaceDelimited": space_delimited_loads,
21+
"pipeDelimited": pipe_delimited_loads,
22+
"deepObject": deep_object_loads,
23+
}
24+
25+
style_deserializers_factory = StyleDeserializersFactory(
26+
style_deserializers=style_deserializers,
27+
)

Diff for: openapi_core/deserializing/styles/datatypes.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
from typing import Any
12
from typing import Callable
3+
from typing import Dict
24
from typing import List
5+
from typing import Mapping
36

4-
DeserializerCallable = Callable[[str], List[str]]
7+
DeserializerCallable = Callable[[bool, str, str, Mapping[str, Any]], Any]
8+
StyleDeserializersDict = Dict[str, DeserializerCallable]

Diff for: openapi_core/deserializing/styles/deserializers.py

+14-28
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import Any
33
from typing import Callable
44
from typing import List
5+
from typing import Mapping
56
from typing import Optional
67

78
from jsonschema_path import SchemaPath
@@ -11,46 +12,31 @@
1112
from openapi_core.deserializing.styles.exceptions import (
1213
EmptyQueryParameterValue,
1314
)
14-
from openapi_core.schema.parameters import get_aslist
15-
from openapi_core.schema.parameters import get_explode
1615

1716

18-
class CallableStyleDeserializer:
17+
class StyleDeserializer:
1918
def __init__(
2019
self,
21-
param_or_header: SchemaPath,
2220
style: str,
21+
explode: bool,
22+
name: str,
23+
schema_type: str,
2324
deserializer_callable: Optional[DeserializerCallable] = None,
2425
):
25-
self.param_or_header = param_or_header
2626
self.style = style
27+
self.explode = explode
28+
self.name = name
29+
self.schema_type = schema_type
2730
self.deserializer_callable = deserializer_callable
2831

29-
self.aslist = get_aslist(self.param_or_header)
30-
self.explode = get_explode(self.param_or_header)
31-
32-
def deserialize(self, value: Any) -> Any:
32+
def deserialize(self, location: Mapping[str, Any]) -> Any:
3333
if self.deserializer_callable is None:
3434
warnings.warn(f"Unsupported {self.style} style")
35-
return value
36-
37-
# if "in" not defined then it's a Header
38-
if "allowEmptyValue" in self.param_or_header:
39-
warnings.warn(
40-
"Use of allowEmptyValue property is deprecated",
41-
DeprecationWarning,
42-
)
43-
allow_empty_values = self.param_or_header.getkey(
44-
"allowEmptyValue", False
45-
)
46-
location_name = self.param_or_header.getkey("in", "header")
47-
if location_name == "query" and value == "" and not allow_empty_values:
48-
name = self.param_or_header["name"]
49-
raise EmptyQueryParameterValue(name)
35+
return location[self.name]
5036

51-
if not self.aslist or self.explode:
52-
return value
5337
try:
54-
return self.deserializer_callable(value)
38+
return self.deserializer_callable(
39+
self.explode, self.name, self.schema_type, location
40+
)
5541
except (ValueError, TypeError, AttributeError):
56-
raise DeserializeError(location_name, self.style, value)
42+
raise DeserializeError(self.style, self.name)

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

+23-14
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,39 @@
11
import re
22
from functools import partial
3+
from typing import Any
34
from typing import Dict
5+
from typing import Mapping
6+
from typing import Optional
47

58
from jsonschema_path import SchemaPath
69

710
from openapi_core.deserializing.styles.datatypes import DeserializerCallable
8-
from openapi_core.deserializing.styles.deserializers import (
9-
CallableStyleDeserializer,
10-
)
11+
from openapi_core.deserializing.styles.datatypes import StyleDeserializersDict
12+
from openapi_core.deserializing.styles.deserializers import StyleDeserializer
1113
from openapi_core.deserializing.styles.util import split
14+
from openapi_core.schema.parameters import get_explode
1215
from openapi_core.schema.parameters import get_style
1316

1417

1518
class StyleDeserializersFactory:
16-
STYLE_DESERIALIZERS: Dict[str, DeserializerCallable] = {
17-
"form": partial(split, separator=","),
18-
"simple": partial(split, separator=","),
19-
"spaceDelimited": partial(split, separator=" "),
20-
"pipeDelimited": partial(split, separator="|"),
21-
"deepObject": partial(re.split, pattern=r"\[|\]"),
22-
}
19+
def __init__(
20+
self,
21+
style_deserializers: Optional[StyleDeserializersDict] = None,
22+
):
23+
if style_deserializers is None:
24+
style_deserializers = {}
25+
self.style_deserializers = style_deserializers
2326

24-
def create(self, param_or_header: SchemaPath) -> CallableStyleDeserializer:
27+
def create(
28+
self, param_or_header: SchemaPath, name: Optional[str] = None
29+
) -> StyleDeserializer:
30+
name = name or param_or_header["name"]
2531
style = get_style(param_or_header)
32+
explode = get_explode(param_or_header)
33+
schema = param_or_header / "schema"
34+
schema_type = schema.getkey("type", "")
2635

27-
deserialize_callable = self.STYLE_DESERIALIZERS.get(style)
28-
return CallableStyleDeserializer(
29-
param_or_header, style, deserialize_callable
36+
deserialize_callable = self.style_deserializers.get(style)
37+
return StyleDeserializer(
38+
style, explode, name, schema_type, deserialize_callable
3039
)

Diff for: openapi_core/deserializing/styles/util.py

+199-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,202 @@
1+
import re
2+
from functools import partial
3+
from typing import Any
14
from typing import List
5+
from typing import Mapping
6+
from typing import Optional
27

8+
from openapi_core.schema.protocols import SuportsGetAll
9+
from openapi_core.schema.protocols import SuportsGetList
310

4-
def split(value: str, separator: str = ",") -> List[str]:
5-
return value.split(separator)
11+
12+
def split(value: str, separator: str = ",", step: int = 1) -> List[str]:
13+
parts = value.split(separator)
14+
15+
if step == 1:
16+
return parts
17+
18+
result = []
19+
for i in range(len(parts)):
20+
if i % step == 0:
21+
if i + 1 < len(parts):
22+
result.append(parts[i] + separator + parts[i + 1])
23+
return result
24+
25+
26+
def delimited_loads(
27+
explode: bool,
28+
name: str,
29+
schema_type: str,
30+
location: Mapping[str, Any],
31+
delimiter: str,
32+
) -> Any:
33+
value = location[name]
34+
35+
explode_type = (explode, schema_type)
36+
if explode_type == (False, "array"):
37+
return split(value, separator=delimiter)
38+
if explode_type == (False, "object"):
39+
return dict(
40+
map(
41+
partial(split, separator=delimiter),
42+
split(value, separator=delimiter, step=2),
43+
)
44+
)
45+
46+
raise ValueError("not available")
47+
48+
49+
def matrix_loads(
50+
explode: bool, name: str, schema_type: str, location: Mapping[str, Any]
51+
) -> Any:
52+
if explode == False:
53+
m = re.match(rf"^;{name}=(.*)$", location[f";{name}"])
54+
if m is None:
55+
raise KeyError(name)
56+
value = m.group(1)
57+
# ;color=blue,black,brown
58+
if schema_type == "array":
59+
return split(value)
60+
# ;color=R,100,G,200,B,150
61+
if schema_type == "object":
62+
return dict(map(split, split(value, step=2)))
63+
# .;color=blue
64+
return value
65+
else:
66+
# ;color=blue;color=black;color=brown
67+
if schema_type == "array":
68+
return re.findall(rf";{name}=([^;]*)", location[f";{name}*"])
69+
# ;R=100;G=200;B=150
70+
if schema_type == "object":
71+
value = location[f";{name}*"]
72+
return dict(
73+
map(
74+
partial(split, separator="="),
75+
split(value[1:], separator=";"),
76+
)
77+
)
78+
# ;color=blue
79+
m = re.match(rf"^;{name}=(.*)$", location[f";{name}*"])
80+
if m is None:
81+
raise KeyError(name)
82+
value = m.group(1)
83+
return value
84+
85+
86+
def label_loads(
87+
explode: bool, name: str, schema_type: str, location: Mapping[str, Any]
88+
) -> Any:
89+
if explode == False:
90+
value = location[f".{name}"]
91+
# .blue,black,brown
92+
if schema_type == "array":
93+
return split(value[1:])
94+
# .R,100,G,200,B,150
95+
if schema_type == "object":
96+
return dict(map(split, split(value[1:], separator=",", step=2)))
97+
# .blue
98+
return value[1:]
99+
else:
100+
value = location[f".{name}*"]
101+
# .blue.black.brown
102+
if schema_type == "array":
103+
return split(value[1:], separator=".")
104+
# .R=100.G=200.B=150
105+
if schema_type == "object":
106+
return dict(
107+
map(
108+
partial(split, separator="="),
109+
split(value[1:], separator="."),
110+
)
111+
)
112+
# .blue
113+
return value[1:]
114+
115+
116+
def form_loads(
117+
explode: bool, name: str, schema_type: str, location: Mapping[str, Any]
118+
) -> Any:
119+
explode_type = (explode, schema_type)
120+
# color=blue,black,brown
121+
if explode_type == (False, "array"):
122+
return split(location[name], separator=",")
123+
# color=blue&color=black&color=brown
124+
elif explode_type == (True, "array"):
125+
if name not in location:
126+
raise KeyError(name)
127+
if isinstance(location, SuportsGetAll):
128+
return location.getall(name)
129+
if isinstance(location, SuportsGetList):
130+
return location.getlist(name)
131+
return location[name]
132+
133+
value = location[name]
134+
# color=R,100,G,200,B,150
135+
if explode_type == (False, "object"):
136+
return dict(map(split, split(value, separator=",", step=2)))
137+
# R=100&G=200&B=150
138+
elif explode_type == (True, "object"):
139+
return dict(
140+
map(partial(split, separator="="), split(value, separator="&"))
141+
)
142+
143+
# color=blue
144+
return value
145+
146+
147+
def simple_loads(
148+
explode: bool, name: str, schema_type: str, location: Mapping[str, Any]
149+
) -> Any:
150+
value = location[name]
151+
152+
# blue,black,brown
153+
if schema_type == "array":
154+
return split(value, separator=",")
155+
156+
explode_type = (explode, schema_type)
157+
# R,100,G,200,B,150
158+
if explode_type == (False, "object"):
159+
return dict(map(split, split(value, separator=",", step=2)))
160+
# R=100,G=200,B=150
161+
elif explode_type == (True, "object"):
162+
return dict(
163+
map(partial(split, separator="="), split(value, separator=","))
164+
)
165+
166+
# blue
167+
return value
168+
169+
170+
def space_delimited_loads(
171+
explode: bool, name: str, schema_type: str, location: Mapping[str, Any]
172+
) -> Any:
173+
return delimited_loads(
174+
explode, name, schema_type, location, delimiter="%20"
175+
)
176+
177+
178+
def pipe_delimited_loads(
179+
explode: bool, name: str, schema_type: str, location: Mapping[str, Any]
180+
) -> Any:
181+
return delimited_loads(explode, name, schema_type, location, delimiter="|")
182+
183+
184+
def deep_object_loads(
185+
explode: bool, name: str, schema_type: str, location: Mapping[str, Any]
186+
) -> Any:
187+
explode_type = (explode, schema_type)
188+
189+
if explode_type != (True, "object"):
190+
raise ValueError("not available")
191+
192+
keys_str = " ".join(location.keys())
193+
if not re.search(rf"{name}\[\w+\]", keys_str):
194+
raise KeyError(name)
195+
196+
values = {}
197+
for key, value in location.items():
198+
# Split the key from the brackets.
199+
key_split = re.split(pattern=r"\[|\]", string=key)
200+
if key_split[0] == name:
201+
values[key_split[1]] = value
202+
return values

0 commit comments

Comments
 (0)