Skip to content

Commit 3a783e1

Browse files
committed
Style deserializing reimplementation
1 parent df1f1e1 commit 3a783e1

File tree

12 files changed

+719
-203
lines changed

12 files changed

+719
-203
lines changed
+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+
)
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]

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)
+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
)
+208-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,211 @@
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+
return value
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
58+
if schema_type == "string":
59+
return value
60+
# ;color=blue,black,brown
61+
if schema_type == "array":
62+
return split(value)
63+
# ;color=R,100,G,200,B,150
64+
if schema_type == "object":
65+
return dict(map(split, split(value, step=2)))
66+
else:
67+
# ;color=blue
68+
if schema_type == "string":
69+
m = re.match(rf"^;{name}=(.*)$", location[f";{name}*"])
70+
if m is None:
71+
raise KeyError(name)
72+
value = m.group(1)
73+
return value
74+
# ;color=blue;color=black;color=brown
75+
if schema_type == "array":
76+
return re.findall(rf";{name}=([^;]*)", location[f";{name}*"])
77+
# ;R=100;G=200;B=150
78+
if schema_type == "object":
79+
value = location[f";{name}*"]
80+
return dict(
81+
map(
82+
partial(split, separator="="),
83+
split(value[1:], separator=";"),
84+
)
85+
)
86+
return location[name]
87+
88+
89+
def label_loads(
90+
explode: bool, name: str, schema_type: str, location: Mapping[str, Any]
91+
) -> Any:
92+
if explode == False:
93+
value = location[f".{name}"]
94+
# .blue
95+
if schema_type == "string":
96+
return value[1:]
97+
# .blue,black,brown
98+
if schema_type == "array":
99+
return split(value[1:])
100+
# .R,100,G,200,B,150
101+
if schema_type == "object":
102+
return dict(map(split, split(value[1:], separator=",", step=2)))
103+
else:
104+
value = location[f".{name}*"]
105+
# .blue
106+
if schema_type == "string":
107+
return value[1:]
108+
# .blue.black.brown
109+
if schema_type == "array":
110+
return split(value[1:], separator=".")
111+
# .R=100.G=200.B=150
112+
if schema_type == "object":
113+
return dict(
114+
map(
115+
partial(split, separator="="),
116+
split(value[1:], separator="."),
117+
)
118+
)
119+
return location[name]
120+
121+
122+
def form_loads(
123+
explode: bool, name: str, schema_type: str, location: Mapping[str, Any]
124+
) -> Any:
125+
if schema_type == "string":
126+
return location[name]
127+
128+
explode_type = (explode, schema_type)
129+
# color=blue,black,brown
130+
if explode_type == (False, "array"):
131+
return split(location[name], separator=",")
132+
# color=blue&color=black&color=brown
133+
elif explode_type == (True, "array"):
134+
if name not in location:
135+
raise KeyError(name)
136+
if isinstance(location, SuportsGetAll):
137+
return location.getall(name)
138+
if isinstance(location, SuportsGetList):
139+
return location.getlist(name)
140+
return location[name]
141+
142+
value = location[name]
143+
# color=R,100,G,200,B,150
144+
if explode_type == (False, "object"):
145+
return dict(map(split, split(value, separator=",", step=2)))
146+
# R=100&G=200&B=150
147+
elif explode_type == (True, "object"):
148+
return dict(
149+
map(partial(split, separator="="), split(value, separator="&"))
150+
)
151+
152+
return value
153+
154+
155+
def simple_loads(
156+
explode: bool, name: str, schema_type: str, location: Mapping[str, Any]
157+
) -> Any:
158+
value = location[name]
159+
if schema_type == "string":
160+
return value
161+
162+
# blue,black,brown
163+
if schema_type == "array":
164+
return split(value, separator=",")
165+
166+
explode_type = (explode, schema_type)
167+
# R,100,G,200,B,150
168+
if explode_type == (False, "object"):
169+
return dict(map(split, split(value, separator=",", step=2)))
170+
# R=100,G=200,B=150
171+
elif explode_type == (True, "object"):
172+
return dict(
173+
map(partial(split, separator="="), split(value, separator=","))
174+
)
175+
176+
return value
177+
178+
179+
def space_delimited_loads(
180+
explode: bool, name: str, schema_type: str, location: Mapping[str, Any]
181+
) -> Any:
182+
return delimited_loads(
183+
explode, name, schema_type, location, delimiter="%20"
184+
)
185+
186+
187+
def pipe_delimited_loads(
188+
explode: bool, name: str, schema_type: str, location: Mapping[str, Any]
189+
) -> Any:
190+
return delimited_loads(explode, name, schema_type, location, delimiter="|")
191+
192+
193+
def deep_object_loads(
194+
explode: bool, name: str, schema_type: str, location: Mapping[str, Any]
195+
) -> Any:
196+
explode_type = (explode, schema_type)
197+
198+
if explode_type != (True, "object"):
199+
return location[name]
200+
201+
keys_str = " ".join(location.keys())
202+
if not re.search(rf"{name}\[\w+\]", keys_str):
203+
raise KeyError(name)
204+
205+
values = {}
206+
for key, value in location.items():
207+
# Split the key from the brackets.
208+
key_split = re.split(pattern=r"\[|\]", string=key)
209+
if key_split[0] == name:
210+
values[key_split[1]] = value
211+
return values

0 commit comments

Comments
 (0)