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
Lines changed: 21 additions & 1 deletion
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+
)
Lines changed: 5 additions & 1 deletion
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

Lines changed: 14 additions & 28 deletions
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)
Lines changed: 23 additions & 14 deletions
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
)
Lines changed: 199 additions & 2 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy