Skip to content

Commit a78c242

Browse files
committed
Response headers support
1 parent c4fab4c commit a78c242

File tree

11 files changed

+263
-72
lines changed

11 files changed

+263
-72
lines changed

openapi_core/deserializing/parameters/deserializers.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import warnings
2+
13
from openapi_core.deserializing.exceptions import DeserializeError
24
from openapi_core.deserializing.parameters.exceptions import (
35
EmptyParameterValue,
@@ -7,19 +9,28 @@
79

810
class PrimitiveDeserializer(object):
911

10-
def __init__(self, param, deserializer_callable):
11-
self.param = param
12+
def __init__(self, param_or_header, deserializer_callable):
13+
self.param_or_header = param_or_header
1214
self.deserializer_callable = deserializer_callable
1315

14-
self.aslist = get_aslist(self.param)
15-
self.explode = get_explode(self.param)
16-
self.style = get_style(self.param)
16+
self.aslist = get_aslist(self.param_or_header)
17+
self.explode = get_explode(self.param_or_header)
18+
self.style = get_style(self.param_or_header)
1719

1820
def __call__(self, value):
19-
if (self.param['in'] == 'query' and value == "" and
20-
not self.param.getkey('allowEmptyValue', False)):
21-
raise EmptyParameterValue(
22-
value, self.style, self.param['name'])
21+
# if "in" not defined then it's a Header
22+
if 'allowEmptyValue' in self.param_or_header:
23+
warnings.warn(
24+
"Use of allowEmptyValue property is deprecated",
25+
DeprecationWarning,
26+
)
27+
allow_empty_values = self.param_or_header.getkey(
28+
'allowEmptyValue', False)
29+
location_name = self.param_or_header.getkey('in', 'header')
30+
if (location_name == 'query' and value == "" and
31+
not allow_empty_values):
32+
name = self.param_or_header.getkey('name', 'header')
33+
raise EmptyParameterValue(value, self.style, name)
2334

2435
if not self.aslist or self.explode:
2536
return value

openapi_core/exceptions.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,32 @@ class OpenAPIError(Exception):
66
pass
77

88

9+
class OpenAPIHeaderError(OpenAPIError):
10+
pass
11+
12+
13+
class MissingHeaderError(OpenAPIHeaderError):
14+
"""Missing header error"""
15+
pass
16+
17+
18+
@attr.s(hash=True)
19+
class MissingHeader(MissingHeaderError):
20+
name = attr.ib()
21+
22+
def __str__(self):
23+
return "Missing header (without default value): {0}".format(
24+
self.name)
25+
26+
27+
@attr.s(hash=True)
28+
class MissingRequiredHeader(MissingHeaderError):
29+
name = attr.ib()
30+
31+
def __str__(self):
32+
return "Missing required header: {0}".format(self.name)
33+
34+
935
class OpenAPIParameterError(OpenAPIError):
1036
pass
1137

openapi_core/schema/parameters.py

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,54 @@
11
from __future__ import division
22

33

4-
def get_aslist(param):
5-
"""Checks if parameter is described as list for simpler scenarios"""
4+
def get_aslist(param_or_header):
5+
"""Checks if parameter/header is described as list for simpler scenarios"""
66
# if schema is not defined it's a complex scenario
7-
if 'schema' not in param:
7+
if 'schema' not in param_or_header:
88
return False
99

10-
param_schema = param / 'schema'
11-
schema_type = param_schema.getkey('type', 'any')
10+
schema = param_or_header / 'schema'
11+
schema_type = schema.getkey('type', 'any')
1212
# TODO: resolve for 'any' schema type
1313
return schema_type in ['array', 'object']
1414

1515

16-
def get_style(param):
17-
"""Checks parameter style for simpler scenarios"""
18-
if 'style' in param:
19-
return param['style']
16+
def get_style(param_or_header):
17+
"""Checks parameter/header style for simpler scenarios"""
18+
if 'style' in param_or_header:
19+
return param_or_header['style']
20+
21+
# if "in" not defined then it's a Header
22+
location = param_or_header.getkey('in', 'header')
2023

2124
# determine default
2225
return (
23-
'simple' if param['in'] in ['path', 'header'] else 'form'
26+
'simple' if location in ['path', 'header'] else 'form'
2427
)
2528

2629

27-
def get_explode(param):
28-
"""Checks parameter explode for simpler scenarios"""
29-
if 'explode' in param:
30-
return param['explode']
30+
def get_explode(param_or_header):
31+
"""Checks parameter/header explode for simpler scenarios"""
32+
if 'explode' in param_or_header:
33+
return param_or_header['explode']
3134

3235
# determine default
33-
style = get_style(param)
36+
style = get_style(param_or_header)
3437
return style == 'form'
38+
39+
40+
def get_value(param_or_header, location, name=None):
41+
"""Returns parameter/header value from specific location"""
42+
name = name or param_or_header['name']
43+
44+
if name not in location:
45+
raise KeyError
46+
47+
aslist = get_aslist(param_or_header)
48+
explode = get_explode(param_or_header)
49+
if aslist and explode:
50+
if hasattr(location, 'getall'):
51+
return location.getall(name)
52+
return location.getlist(name)
53+
54+
return location[name]

openapi_core/testing/responses.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
class MockResponseFactory(object):
66

77
@classmethod
8-
def create(cls, data, status_code=200, mimetype='application/json'):
8+
def create(
9+
cls, data, status_code=200, headers=None,
10+
mimetype='application/json'):
911
return OpenAPIResponse(
1012
data=data,
1113
status_code=status_code,
14+
headers=headers or {},
1215
mimetype=mimetype,
1316
)

openapi_core/validation/request/validators.py

Lines changed: 12 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@
55

66
from openapi_core.casting.schemas.exceptions import CastError
77
from openapi_core.deserializing.exceptions import DeserializeError
8-
from openapi_core.deserializing.parameters.factories import (
9-
ParameterDeserializersFactory,
10-
)
118
from openapi_core.exceptions import (
129
MissingRequiredParameter, MissingParameter,
1310
MissingRequiredRequestBody, MissingRequestBody,
@@ -46,10 +43,6 @@ def schema_unmarshallers_factory(self):
4643
def security_provider_factory(self):
4744
return SecurityProviderFactory()
4845

49-
@property
50-
def parameter_deserializers_factory(self):
51-
return ParameterDeserializersFactory()
52-
5346
def validate(self, request):
5447
try:
5548
path, operation, _, path_result, _ = self._find_path(request)
@@ -177,35 +170,23 @@ def _get_parameters(self, request, params):
177170
return RequestParameters(**locations), errors
178171

179172
def _get_parameter(self, param, request):
180-
if param.getkey('deprecated', False):
173+
name = param['name']
174+
deprecated = param.getkey('deprecated', False)
175+
if deprecated:
181176
warnings.warn(
182-
"{0} parameter is deprecated".format(param['name']),
177+
"{0} parameter is deprecated".format(name),
183178
DeprecationWarning,
184179
)
185180

181+
param_location = param['in']
182+
location = request.parameters[param_location]
186183
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
184+
return self._get_param_or_header_value(param, location)
185+
except KeyError:
186+
required = param.getkey('required', False)
187+
if required:
188+
raise MissingRequiredParameter(name)
189+
raise MissingParameter(name)
209190

210191
def _get_body(self, request, operation):
211192
if 'requestBody' not in operation:
@@ -280,7 +261,3 @@ def _get_body_value(self, request_body, request):
280261
raise MissingRequiredRequestBody(request)
281262
raise MissingRequestBody(request)
282263
return request.body
283-
284-
def _deserialise_parameter(self, param, value):
285-
deserializer = self.parameter_deserializers_factory.create(param)
286-
return deserializer(value)

openapi_core/validation/response/datatypes.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""OpenAPI core validation response datatypes module"""
22
import attr
3+
from werkzeug.datastructures import Headers
34

45
from openapi_core.validation.datatypes import BaseValidationResult
56

@@ -13,14 +14,15 @@ class OpenAPIResponse(object):
1314
The response body, as string.
1415
status_code
1516
The status code as integer.
17+
headers
18+
Response headers as Headers.
1619
mimetype
1720
Lowercase content type without charset.
1821
"""
19-
2022
data = attr.ib()
2123
status_code = attr.ib()
22-
2324
mimetype = attr.ib()
25+
headers = attr.ib(factory=Headers, converter=Headers)
2426

2527

2628
@attr.s

openapi_core/validation/response/validators.py

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
"""OpenAPI core validation response validators module"""
22
from __future__ import division
3+
import warnings
34

45
from openapi_core.casting.schemas.exceptions import CastError
56
from openapi_core.deserializing.exceptions import DeserializeError
6-
from openapi_core.exceptions import MissingResponseContent
7+
from openapi_core.exceptions import (
8+
MissingHeader, MissingRequiredHeader, MissingResponseContent,
9+
)
710
from openapi_core.templating.media_types.exceptions import MediaTypeFinderError
811
from openapi_core.templating.paths.exceptions import PathError
912
from openapi_core.templating.responses.exceptions import ResponseFinderError
@@ -117,12 +120,48 @@ def _get_data(self, response, operation_response):
117120
return data, []
118121

119122
def _get_headers(self, response, operation_response):
120-
errors = []
123+
if 'headers' not in operation_response:
124+
return {}, []
121125

122-
# @todo: implement
123-
headers = {}
126+
headers = operation_response / 'headers'
124127

125-
return headers, errors
128+
errors = []
129+
validated = {}
130+
for name, header in headers.items():
131+
# ignore Content-Type header
132+
if name.lower() == "content-type":
133+
continue
134+
try:
135+
value = self._get_header(name, header, response)
136+
except MissingHeader:
137+
continue
138+
except (
139+
MissingRequiredHeader, DeserializeError,
140+
CastError, ValidateError, UnmarshalError,
141+
) as exc:
142+
errors.append(exc)
143+
continue
144+
else:
145+
validated[name] = value
146+
147+
return validated, errors
148+
149+
def _get_header(self, name, header, response):
150+
deprecated = header.getkey('deprecated', False)
151+
if deprecated:
152+
warnings.warn(
153+
"{0} header is deprecated".format(name),
154+
DeprecationWarning,
155+
)
156+
157+
try:
158+
return self._get_param_or_header_value(
159+
header, response.headers, name=name)
160+
except KeyError:
161+
required = header.getkey('required', False)
162+
if required:
163+
raise MissingRequiredHeader(name)
164+
raise MissingHeader(name)
126165

127166
def _get_data_value(self, response):
128167
if not response.data:

openapi_core/validation/validators.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
from openapi_core.deserializing.media_types.factories import (
66
MediaTypeDeserializersFactory,
77
)
8+
from openapi_core.deserializing.parameters.factories import (
9+
ParameterDeserializersFactory,
10+
)
11+
from openapi_core.schema.parameters import get_value
812
from openapi_core.templating.paths.finders import PathFinder
913
from openapi_core.unmarshalling.schemas.util import build_format_checker
1014

@@ -36,6 +40,10 @@ def media_type_deserializers_factory(self):
3640
return MediaTypeDeserializersFactory(
3741
self.custom_media_type_deserializers)
3842

43+
@property
44+
def parameter_deserializers_factory(self):
45+
return ParameterDeserializersFactory()
46+
3947
@property
4048
def schema_unmarshallers_factory(self):
4149
raise NotImplementedError
@@ -52,10 +60,40 @@ def _deserialise_data(self, mimetype, value):
5260
deserializer = self.media_type_deserializers_factory.create(mimetype)
5361
return deserializer(value)
5462

63+
def _deserialise_parameter(self, param, value):
64+
deserializer = self.parameter_deserializers_factory.create(param)
65+
return deserializer(value)
66+
5567
def _cast(self, schema, value):
5668
caster = self.schema_casters_factory.create(schema)
5769
return caster(value)
5870

5971
def _unmarshal(self, schema, value):
6072
unmarshaller = self.schema_unmarshallers_factory.create(schema)
6173
return unmarshaller(value)
74+
75+
def _get_param_or_header_value(self, param_or_header, location, name=None):
76+
try:
77+
raw_value = get_value(param_or_header, location, name=name)
78+
except KeyError:
79+
if 'schema' not in param_or_header:
80+
raise
81+
schema = param_or_header / 'schema'
82+
if 'default' not in schema:
83+
raise
84+
casted = schema['default']
85+
else:
86+
# Simple scenario
87+
if 'content' not in param_or_header:
88+
deserialised = self._deserialise_parameter(
89+
param_or_header, raw_value)
90+
schema = param_or_header / 'schema'
91+
# Complex scenario
92+
else:
93+
content = param_or_header / 'content'
94+
mimetype, media_type = next(content.items())
95+
deserialised = self._deserialise_data(mimetype, raw_value)
96+
schema = media_type / 'schema'
97+
casted = self._cast(schema, deserialised)
98+
unmarshalled = self._unmarshal(schema, casted)
99+
return unmarshalled

0 commit comments

Comments
 (0)