Skip to content

Commit cd3d3be

Browse files
committed
Parameter deserialize complex scenario support
1 parent 85cf602 commit cd3d3be

File tree

8 files changed

+174
-68
lines changed

8 files changed

+174
-68
lines changed

openapi_core/deserializing/parameters/factories.py

-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import warnings
2-
31
from openapi_core.deserializing.parameters.deserializers import (
42
PrimitiveDeserializer,
53
)
@@ -16,12 +14,6 @@ class ParameterDeserializersFactory(object):
1614
}
1715

1816
def create(self, param):
19-
if param.getkey('deprecated', False):
20-
warnings.warn(
21-
"{0} parameter is deprecated".format(param['name']),
22-
DeprecationWarning,
23-
)
24-
2517
style = get_style(param)
2618

2719
deserialize_callable = self.PARAMETER_STYLE_DESERIALIZERS[style]

openapi_core/validation/request/validators.py

+45-29
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""OpenAPI core validation request validators module"""
22
from __future__ import division
33
from itertools import chain
4+
import warnings
45

56
from openapi_core.casting.schemas.exceptions import CastError
67
from openapi_core.deserializing.exceptions import DeserializeError
@@ -160,41 +161,52 @@ def _get_parameters(self, request, params):
160161
continue
161162
seen.add((param_name, param_location))
162163
try:
163-
raw_value = self._get_parameter_value(param, request)
164-
except MissingRequiredParameter as exc:
165-
errors.append(exc)
166-
continue
164+
value = self._get_parameter(param, request)
167165
except MissingParameter:
168-
if 'schema' not in param:
169-
continue
170-
schema = param / 'schema'
171-
if 'default' not in schema:
172-
continue
173-
casted = schema['default']
174-
else:
175-
try:
176-
deserialised = self._deserialise_parameter(
177-
param, raw_value)
178-
except DeserializeError as exc:
179-
errors.append(exc)
180-
continue
181-
182-
try:
183-
casted = self._cast(param, deserialised)
184-
except CastError as exc:
185-
errors.append(exc)
186-
continue
187-
188-
try:
189-
unmarshalled = self._unmarshal(param, casted)
190-
except (ValidateError, UnmarshalError) as exc:
166+
continue
167+
except (
168+
MissingRequiredParameter, DeserializeError,
169+
CastError, ValidateError, UnmarshalError,
170+
) as exc:
191171
errors.append(exc)
172+
continue
192173
else:
193174
locations.setdefault(param_location, {})
194-
locations[param_location][param_name] = unmarshalled
175+
locations[param_location][param_name] = value
195176

196177
return RequestParameters(**locations), errors
197178

179+
def _get_parameter(self, param, request):
180+
if param.getkey('deprecated', False):
181+
warnings.warn(
182+
"{0} parameter is deprecated".format(param['name']),
183+
DeprecationWarning,
184+
)
185+
186+
try:
187+
raw_value = self._get_parameter_value(param, request)
188+
except MissingParameter:
189+
if 'schema' not in param:
190+
raise
191+
schema = param / 'schema'
192+
if 'default' not in schema:
193+
raise
194+
casted = schema['default']
195+
else:
196+
# Simple scenario
197+
if 'content' not in param:
198+
deserialised = self._deserialise_parameter(param, raw_value)
199+
schema = param / 'schema'
200+
# Complex scenario
201+
else:
202+
content = param / 'content'
203+
mimetype, media_type = next(content.items())
204+
deserialised = self._deserialise_data(mimetype, raw_value)
205+
schema = media_type / 'schema'
206+
casted = self._cast(schema, deserialised)
207+
unmarshalled = self._unmarshal(schema, casted)
208+
return unmarshalled
209+
198210
def _get_body(self, request, operation):
199211
if 'requestBody' not in operation:
200212
return None, []
@@ -224,8 +236,12 @@ def _get_body(self, request, operation):
224236
except CastError as exc:
225237
return None, [exc, ]
226238

239+
if 'schema' not in media_type:
240+
return casted, []
241+
242+
schema = media_type / 'schema'
227243
try:
228-
body = self._unmarshal(media_type, casted)
244+
body = self._unmarshal(schema, casted)
229245
except (ValidateError, UnmarshalError) as exc:
230246
return None, [exc, ]
231247

openapi_core/validation/response/validators.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,12 @@ def _get_data(self, response, operation_response):
105105
except CastError as exc:
106106
return None, [exc, ]
107107

108+
if 'schema' not in media_type:
109+
return casted, []
110+
111+
schema = media_type / 'schema'
108112
try:
109-
data = self._unmarshal(media_type, casted)
113+
data = self._unmarshal(schema, casted)
110114
except (ValidateError, UnmarshalError) as exc:
111115
return None, [exc, ]
112116

openapi_core/validation/validators.py

+2-11
Original file line numberDiff line numberDiff line change
@@ -52,19 +52,10 @@ def _deserialise_data(self, mimetype, value):
5252
deserializer = self.media_type_deserializers_factory.create(mimetype)
5353
return deserializer(value)
5454

55-
def _cast(self, param_or_media_type, value):
56-
# return param_or_media_type.cast(value)
57-
if 'schema' not in param_or_media_type:
58-
return value
59-
60-
schema = param_or_media_type / 'schema'
55+
def _cast(self, schema, value):
6156
caster = self.schema_casters_factory.create(schema)
6257
return caster(value)
6358

64-
def _unmarshal(self, param_or_media_type, value):
65-
if 'schema' not in param_or_media_type:
66-
return value
67-
68-
schema = param_or_media_type / 'schema'
59+
def _unmarshal(self, schema, value):
6960
unmarshaller = self.schema_unmarshallers_factory.create(schema)
7061
return unmarshaller(value)

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

+22-2
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,17 @@ paths:
8383
type: object
8484
required:
8585
- lat
86-
- long
86+
- lon
8787
properties:
8888
lat:
8989
type: number
90-
long:
90+
lon:
9191
type: number
9292
responses:
9393
'200':
9494
$ref: "#/components/responses/PetsResponse"
95+
'404':
96+
$ref: "#/components/responses/HtmlResponse"
9597
post:
9698
summary: Create a pet
9799
description: Creates new pet entry
@@ -119,6 +121,13 @@ paths:
119121
type: integer
120122
format: int32
121123
required: true
124+
- name: userdata
125+
in: cookie
126+
content:
127+
application/json:
128+
schema:
129+
$ref: '#/components/schemas/Userdata'
130+
required: false
122131
requestBody:
123132
required: true
124133
content:
@@ -220,6 +229,13 @@ paths:
220229
$ref: "#/components/responses/ErrorResponse"
221230
components:
222231
schemas:
232+
Userdata:
233+
type: object
234+
required:
235+
- name
236+
properties:
237+
name:
238+
type: string
223239
Utctime:
224240
oneOf:
225241
- type: string
@@ -406,6 +422,10 @@ components:
406422
application/json:
407423
schema:
408424
$ref: "#/components/schemas/ExtendedError"
425+
HtmlResponse:
426+
description: HTML page
427+
content:
428+
text/html: {}
409429
PetsResponse:
410430
description: An paged array of pets
411431
headers:

tests/integration/validation/test_petstore.py

+40-3
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,38 @@ def test_get_pets_response(self, spec, response_validator):
139139
assert response_result.data.data[0].id == 1
140140
assert response_result.data.data[0].name == 'Cat'
141141

142+
def test_get_pets_response_no_schema(self, spec, response_validator):
143+
host_url = 'http://petstore.swagger.io/v1'
144+
path_pattern = '/v1/pets'
145+
query_params = {
146+
'limit': '20',
147+
}
148+
149+
request = MockRequest(
150+
host_url, 'GET', '/pets',
151+
path_pattern=path_pattern, args=query_params,
152+
)
153+
154+
parameters = validate_parameters(spec, request)
155+
body = validate_body(spec, request)
156+
157+
assert parameters == RequestParameters(
158+
query={
159+
'limit': 20,
160+
'page': 1,
161+
'search': '',
162+
}
163+
)
164+
assert body is None
165+
166+
data = '<html></html>'
167+
response = MockResponse(data, status_code=404, mimetype='text/html')
168+
169+
response_result = response_validator.validate(request, response)
170+
171+
assert response_result.errors == []
172+
assert response_result.data == data
173+
142174
def test_get_pets_invalid_response(self, spec, response_validator):
143175
host_url = 'http://petstore.swagger.io/v1'
144176
path_pattern = '/v1/pets'
@@ -393,9 +425,6 @@ def test_get_pets_param_order(self, spec):
393425

394426
assert body is None
395427

396-
@pytest.mark.xfail(
397-
reason="No parameters deserialization support for complex scenarios"
398-
)
399428
def test_get_pets_param_coordinates(self, spec):
400429
host_url = 'http://petstore.swagger.io/v1'
401430
path_pattern = '/v1/pets'
@@ -453,8 +482,13 @@ def test_post_birds(self, spec, spec_dict):
453482
headers = {
454483
'api_key': self.api_key_encoded,
455484
}
485+
userdata = {
486+
'name': 'user1',
487+
}
488+
userdata_json = json.dumps(userdata)
456489
cookies = {
457490
'user': '123',
491+
'userdata': userdata_json,
458492
}
459493

460494
request = MockRequest(
@@ -471,6 +505,9 @@ def test_post_birds(self, spec, spec_dict):
471505
},
472506
cookie={
473507
'user': 123,
508+
'userdata': {
509+
'name': 'user1',
510+
},
474511
},
475512
)
476513

tests/integration/validation/test_validators.py

+60
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,66 @@ def test_invalid_content_type(self, validator):
194194
},
195195
)
196196

197+
def test_invalid_complex_parameter(self, validator, spec_dict):
198+
pet_name = 'Cat'
199+
pet_tag = 'cats'
200+
pet_street = 'Piekna'
201+
pet_city = 'Warsaw'
202+
data_json = {
203+
'name': pet_name,
204+
'tag': pet_tag,
205+
'position': 2,
206+
'address': {
207+
'street': pet_street,
208+
'city': pet_city,
209+
},
210+
'ears': {
211+
'healthy': True,
212+
}
213+
}
214+
data = json.dumps(data_json)
215+
headers = {
216+
'api_key': self.api_key_encoded,
217+
}
218+
userdata = {
219+
'name': 1,
220+
}
221+
userdata_json = json.dumps(userdata)
222+
cookies = {
223+
'user': '123',
224+
'userdata': userdata_json,
225+
}
226+
request = MockRequest(
227+
'https://development.gigantic-server.com', 'post', '/v1/pets',
228+
path_pattern='/v1/pets', data=data,
229+
headers=headers, cookies=cookies,
230+
)
231+
232+
result = validator.validate(request)
233+
234+
assert len(result.errors) == 1
235+
assert type(result.errors[0]) == InvalidSchemaValue
236+
assert result.parameters == RequestParameters(
237+
header={
238+
'api_key': self.api_key,
239+
},
240+
cookie={
241+
'user': 123,
242+
},
243+
)
244+
assert result.security == {}
245+
246+
schemas = spec_dict['components']['schemas']
247+
pet_model = schemas['PetCreate']['x-model']
248+
address_model = schemas['Address']['x-model']
249+
assert result.body.__class__.__name__ == pet_model
250+
assert result.body.name == pet_name
251+
assert result.body.tag == pet_tag
252+
assert result.body.position == 2
253+
assert result.body.address.__class__.__name__ == address_model
254+
assert result.body.address.street == pet_street
255+
assert result.body.address.city == pet_city
256+
197257
def test_post_pets(self, validator, spec_dict):
198258
pet_name = 'Cat'
199259
pet_tag = 'cats'

tests/unit/deserializing/test_parameters_deserializers.py

-14
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,6 @@ def create_deserializer(param):
1717
return ParameterDeserializersFactory().create(param)
1818
return create_deserializer
1919

20-
def test_deprecated(self, deserializer_factory):
21-
spec = {
22-
'name': 'param',
23-
'in': 'query',
24-
'deprecated': True,
25-
}
26-
param = SpecPath.from_spec(spec)
27-
value = 'test'
28-
29-
with pytest.warns(DeprecationWarning):
30-
result = deserializer_factory(param)(value)
31-
32-
assert result == value
33-
3420
def test_query_empty(self, deserializer_factory):
3521
spec = {
3622
'name': 'param',

0 commit comments

Comments
 (0)