Skip to content

Commit fd450e6

Browse files
committed
Parameter deserializers
1 parent 2d50e92 commit fd450e6

File tree

6 files changed

+170
-67
lines changed

6 files changed

+170
-67
lines changed

openapi_core/parameters.py

Lines changed: 75 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,23 @@
22
import logging
33
import warnings
44

5+
from functools import lru_cache
56
from six import iteritems
67

7-
from openapi_core.enums import ParameterLocation, ParameterStyle
8+
from openapi_core.enums import ParameterLocation, ParameterStyle, SchemaType
89
from openapi_core.exceptions import (
910
EmptyValue, InvalidValueType, InvalidParameterValue,
1011
)
1112

1213
log = logging.getLogger(__name__)
1314

15+
PARAMETER_STYLE_DESERIALIZERS = {
16+
ParameterStyle.FORM: lambda x: x.split(','),
17+
ParameterStyle.SIMPLE: lambda x: x.split(','),
18+
ParameterStyle.SPACE_DELIMITED: lambda x: x.split(' '),
19+
ParameterStyle.PIPE_DELIMITED: lambda x: x.split('|'),
20+
}
21+
1422

1523
class Parameter(object):
1624
"""Represents an OpenAPI operation Parameter."""
@@ -32,7 +40,14 @@ def __init__(
3240
)
3341
self.items = items
3442
self.style = ParameterStyle(style or self.default_style)
35-
self.explode = explode or self.default_explode
43+
self.explode = self.default_explode if explode is None else explode
44+
45+
@property
46+
def aslist(self):
47+
return (
48+
self.schema and
49+
self.schema.type in [SchemaType.ARRAY, SchemaType.OBJECT]
50+
)
3651

3752
@property
3853
def default_style(self):
@@ -45,6 +60,16 @@ def default_style(self):
4560
def default_explode(self):
4661
return self.style == ParameterStyle.FORM
4762

63+
def get_dererializer(self):
64+
return PARAMETER_STYLE_DESERIALIZERS[self.style]
65+
66+
def deserialize(self, value):
67+
if not self.aslist or self.explode:
68+
return value
69+
70+
deserializer = self.get_dererializer()
71+
return deserializer(value)
72+
4873
def unmarshal(self, value):
4974
if self.deprecated:
5075
warnings.warn(
@@ -60,65 +85,65 @@ def unmarshal(self, value):
6085
if not self.schema:
6186
return value
6287

88+
deserialized = self.deserialize(value)
89+
6390
try:
64-
return self.schema.unmarshal(value)
91+
return self.schema.unmarshal(deserialized)
6592
except InvalidValueType as exc:
6693
raise InvalidParameterValue(str(exc))
6794

6895

96+
class ParameterFactory(object):
97+
98+
def __init__(self, dereferencer, schemas_registry):
99+
self.dereferencer = dereferencer
100+
self.schemas_registry = schemas_registry
101+
102+
def create(self, parameter_spec, parameter_name=None):
103+
parameter_deref = self.dereferencer.dereference(parameter_spec)
104+
105+
parameter_name = parameter_name or parameter_deref['name']
106+
parameter_in = parameter_deref.get('in', 'header')
107+
108+
allow_empty_value = parameter_deref.get('allowEmptyValue')
109+
required = parameter_deref.get('required', False)
110+
111+
style = parameter_deref.get('style')
112+
explode = parameter_deref.get('explode')
113+
114+
schema_spec = parameter_deref.get('schema', None)
115+
schema = None
116+
if schema_spec:
117+
schema, _ = self.schemas_registry.get_or_create(schema_spec)
118+
119+
return Parameter(
120+
parameter_name, parameter_in,
121+
schema=schema, required=required,
122+
allow_empty_value=allow_empty_value,
123+
style=style, explode=explode,
124+
)
125+
126+
69127
class ParametersGenerator(object):
70128

71129
def __init__(self, dereferencer, schemas_registry):
72130
self.dereferencer = dereferencer
73131
self.schemas_registry = schemas_registry
74132

75133
def generate(self, parameters):
76-
for parameter_name, parameter in iteritems(parameters):
77-
parameter_deref = self.dereferencer.dereference(parameter)
78-
79-
parameter_in = parameter_deref.get('in', 'header')
80-
81-
allow_empty_value = parameter_deref.get('allowEmptyValue')
82-
required = parameter_deref.get('required', False)
83-
84-
schema_spec = parameter_deref.get('schema', None)
85-
schema = None
86-
if schema_spec:
87-
schema, _ = self.schemas_registry.get_or_create(schema_spec)
88-
89-
yield (
90-
parameter_name,
91-
Parameter(
92-
parameter_name, parameter_in,
93-
schema=schema, required=required,
94-
allow_empty_value=allow_empty_value,
95-
),
96-
)
134+
for parameter_name, parameter_spec in iteritems(parameters):
135+
parameter = self.parameter_factory.create(
136+
parameter_spec, parameter_name=parameter_name)
137+
138+
yield (parameter_name, parameter)
97139

98140
def generate_from_list(self, parameters_list):
99-
for parameter in parameters_list:
100-
parameter_deref = self.dereferencer.dereference(parameter)
101-
102-
parameter_name = parameter_deref['name']
103-
parameter_in = parameter_deref.get('in', 'header')
104-
105-
allow_empty_value = parameter_deref.get('allowEmptyValue')
106-
required = parameter_deref.get('required', False)
107-
108-
style = parameter_deref.get('style')
109-
explode = parameter_deref.get('explode')
110-
111-
schema_spec = parameter_deref.get('schema', None)
112-
schema = None
113-
if schema_spec:
114-
schema, _ = self.schemas_registry.get_or_create(schema_spec)
115-
116-
yield (
117-
parameter_name,
118-
Parameter(
119-
parameter_name, parameter_in,
120-
schema=schema, required=required,
121-
allow_empty_value=allow_empty_value,
122-
style=style, explode=explode,
123-
),
124-
)
141+
for parameter_spec in parameters_list:
142+
parameter = self.parameter_factory.create(parameter_spec)
143+
144+
yield (parameter.name, parameter)
145+
146+
@property
147+
@lru_cache()
148+
def parameter_factory(self):
149+
return ParameterFactory(self.dereferencer, self.schemas_registry)

openapi_core/validators.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
"""OpenAPI core validators module"""
22
from six import iteritems
33

4-
from openapi_core.enums import ParameterLocation
54
from openapi_core.exceptions import (
65
OpenAPIMappingError, MissingParameter, MissingBody, InvalidResponse,
76
)
87

98

109
class RequestParameters(dict):
1110

11+
valid_locations = ['path', 'query', 'headers', 'cookies']
12+
1213
def __getitem__(self, location):
1314
self.validate_location(location)
1415

@@ -19,7 +20,7 @@ def __setitem__(self, location, value):
1920

2021
@classmethod
2122
def validate_location(cls, location):
22-
if not ParameterLocation.has_value(location):
23+
if location not in cls.valid_locations:
2324
raise OpenAPIMappingError(
2425
"Unknown parameter location: {0}".format(str(location)))
2526

@@ -116,12 +117,19 @@ def validate(self, request):
116117
return RequestValidationResult(errors, body, parameters)
117118

118119
def _get_raw_value(self, request, param):
120+
location = request.parameters[param.location.value]
121+
119122
try:
120-
return request.parameters[param.location.value][param.name]
123+
raw = request.parameters[param.location.value][param.name]
121124
except KeyError:
122125
raise MissingParameter(
123126
"Missing required `{0}` parameter".format(param.name))
124127

128+
if param.aslist and param.explode:
129+
return location.getlist(param.name)
130+
131+
return raw
132+
125133
def _get_raw_body(self, request):
126134
if not request.body:
127135
raise MissingBody("Missing required request body")

openapi_core/wrappers.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import warnings
33

44
from six.moves.urllib.parse import urljoin
5+
from werkzeug.datastructures import ImmutableMultiDict
56

67

78
class BaseOpenAPIRequest(object):
@@ -54,9 +55,9 @@ def __init__(
5455

5556
self.parameters = {
5657
'path': view_args or {},
57-
'query': args or {},
58-
'headers': headers or {},
59-
'cookies': cookies or {},
58+
'query': ImmutableMultiDict(args or []),
59+
'header': headers or {},
60+
'cookie': cookies or {},
6061
}
6162

6263
self.body = data or ''

tests/integration/data/v3.0/petstore.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ paths:
4949
items:
5050
type: integer
5151
format: int32
52+
- name: tags
53+
in: query
54+
description: Filter pets with tags
55+
schema:
56+
type: array
57+
items:
58+
$ref: "#/components/schemas/Tag"
59+
explode: false
5260
responses:
5361
'200':
5462
description: An paged array of pets

tests/integration/test_petstore.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,6 @@ def test_get_pets(self, spec, response_validator):
181181
path_pattern = '/v1/pets'
182182
query_params = {
183183
'limit': '20',
184-
'ids': ['12', '13'],
185184
}
186185

187186
request = MockRequest(
@@ -197,7 +196,6 @@ def test_get_pets(self, spec, response_validator):
197196
'limit': 20,
198197
'page': 1,
199198
'search': '',
200-
'ids': [12, 13],
201199
}
202200
}
203201
assert body is None
@@ -213,7 +211,7 @@ def test_get_pets(self, spec, response_validator):
213211
assert response_result.errors == []
214212
assert response_result.data == data_json
215213

216-
def test_get_pets_tag_param(self, spec, response_validator):
214+
def test_get_pets_ids_param(self, spec, response_validator):
217215
host_url = 'http://petstore.swagger.io/v1'
218216
path_pattern = '/v1/pets'
219217
query_params = {
@@ -250,6 +248,43 @@ def test_get_pets_tag_param(self, spec, response_validator):
250248
assert response_result.errors == []
251249
assert response_result.data == data_json
252250

251+
def test_get_pets_tags_param(self, spec, response_validator):
252+
host_url = 'http://petstore.swagger.io/v1'
253+
path_pattern = '/v1/pets'
254+
query_params = [
255+
('limit', '20'),
256+
('tags', 'cats,dogs'),
257+
]
258+
259+
request = MockRequest(
260+
host_url, 'GET', '/pets',
261+
path_pattern=path_pattern, args=query_params,
262+
)
263+
264+
parameters = request.get_parameters(spec)
265+
body = request.get_body(spec)
266+
267+
assert parameters == {
268+
'query': {
269+
'limit': 20,
270+
'page': 1,
271+
'search': '',
272+
'tags': ['cats', 'dogs'],
273+
}
274+
}
275+
assert body is None
276+
277+
data_json = {
278+
'data': [],
279+
}
280+
data = json.dumps(data_json)
281+
response = MockResponse(data)
282+
283+
response_result = response_validator.validate(request, response)
284+
285+
assert response_result.errors == []
286+
assert response_result.data == data_json
287+
253288
def test_get_pets_wrong_parameter_type(self, spec):
254289
host_url = 'http://petstore.swagger.io/v1'
255290
path_pattern = '/v1/pets'

0 commit comments

Comments
 (0)