Skip to content

Commit 9bdb13f

Browse files
committed
Request validator
1 parent a61a249 commit 9bdb13f

File tree

6 files changed

+326
-2
lines changed

6 files changed

+326
-2
lines changed

openapi_core/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ class MissingParameterError(OpenAPIMappingError):
1313
pass
1414

1515

16+
class MissingBodyError(OpenAPIMappingError):
17+
pass
18+
19+
1620
class MissingPropertyError(OpenAPIMappingError):
1721
pass
1822

openapi_core/request_bodies.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""OpenAPI core requestBodies module"""
22
from functools import lru_cache
33

4+
from openapi_core.exceptions import InvalidContentTypeError
45
from openapi_core.media_types import MediaTypeGenerator
56

67

@@ -12,7 +13,11 @@ def __init__(self, content, required=False):
1213
self.required = required
1314

1415
def __getitem__(self, mimetype):
15-
return self.content[mimetype]
16+
try:
17+
return self.content[mimetype]
18+
except KeyError:
19+
raise InvalidContentTypeError(
20+
"Invalid mime type `{0}`".format(mimetype))
1621

1722

1823
class RequestBodyFactory(object):

openapi_core/specs.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from openapi_spec_validator import openapi_v3_spec_validator
77

88
from openapi_core.components import ComponentsFactory
9-
from openapi_core.exceptions import InvalidOperationError
9+
from openapi_core.exceptions import InvalidOperationError, InvalidServerError
1010
from openapi_core.infos import InfoFactory
1111
from openapi_core.paths import PathsGenerator
1212
from openapi_core.schemas import SchemaRegistry
@@ -32,6 +32,14 @@ def __getitem__(self, path_name):
3232
def default_url(self):
3333
return self.servers[0].default_url
3434

35+
def get_server(self, full_url_pattern):
36+
for spec_server in self.servers:
37+
if spec_server.default_url in full_url_pattern:
38+
return spec_server
39+
40+
raise InvalidServerError(
41+
"Invalid request server {0}".format(full_url_pattern))
42+
3543
def get_server_url(self, index=0):
3644
return self.servers[index].default_url
3745

openapi_core/validators.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
"""OpenAPI core validators module"""
2+
from six import iteritems
3+
4+
from openapi_core.exceptions import (
5+
OpenAPIMappingError, MissingParameterError, MissingBodyError,
6+
)
7+
8+
9+
class RequestParameters(dict):
10+
11+
valid_locations = ['path', 'query', 'headers', 'cookies']
12+
13+
def __getitem__(self, location):
14+
self.validate_location(location)
15+
16+
return self.setdefault(location, {})
17+
18+
def __setitem__(self, location, value):
19+
raise NotImplementedError
20+
21+
@classmethod
22+
def validate_location(cls, location):
23+
if location not in cls.valid_locations:
24+
raise OpenAPIMappingError(
25+
"Unknown parameter location: {0}".format(str(location)))
26+
27+
28+
class BaseValidationResult(object):
29+
30+
def __init__(self, errors):
31+
self.errors = errors
32+
33+
def validate(self):
34+
for error in self.errors:
35+
raise error
36+
37+
38+
class RequestValidationResult(BaseValidationResult):
39+
40+
def __init__(self, errors, body=None, parameters=None):
41+
super(RequestValidationResult, self).__init__(errors)
42+
self.body = body
43+
self.parameters = parameters or RequestParameters()
44+
45+
46+
class RequestValidator(object):
47+
48+
SPEC_LOCATION_TO_REQUEST_LOCATION = {
49+
'path': 'view_args',
50+
'query': 'args',
51+
'headers': 'headers',
52+
'cookies': 'cookies',
53+
}
54+
55+
def __init__(self, spec):
56+
self.spec = spec
57+
58+
def validate(self, request):
59+
errors = []
60+
body = None
61+
parameters = RequestParameters()
62+
63+
try:
64+
server = self.spec.get_server(request.full_url_pattern)
65+
# don't process if server errors
66+
except OpenAPIMappingError as exc:
67+
errors.append(exc)
68+
return RequestValidationResult(errors, body, parameters)
69+
70+
operation_pattern = request.full_url_pattern.replace(
71+
server.default_url, '')
72+
method = request.method.lower()
73+
74+
try:
75+
operation = self.spec.get_operation(operation_pattern, method)
76+
# don't process if operation errors
77+
except OpenAPIMappingError as exc:
78+
errors.append(exc)
79+
return RequestValidationResult(errors, body, parameters)
80+
81+
for param_name, param in iteritems(operation.parameters):
82+
try:
83+
raw_value = self._get_raw_value(request, param)
84+
except MissingParameterError as exc:
85+
if param.required:
86+
errors.append(exc)
87+
88+
if not param.schema or param.schema.default is None:
89+
continue
90+
raw_value = param.schema.default
91+
92+
value = param.unmarshal(raw_value)
93+
94+
parameters[param.location][param_name] = value
95+
96+
if operation.request_body is not None:
97+
try:
98+
media_type = operation.request_body[request.mimetype]
99+
except OpenAPIMappingError as exc:
100+
errors.append(exc)
101+
else:
102+
try:
103+
raw_body = self._get_raw_body(request)
104+
except MissingBodyError as exc:
105+
if operation.request_body.required:
106+
errors.append(exc)
107+
else:
108+
body = media_type.unmarshal(raw_body)
109+
110+
return RequestValidationResult(errors, body, parameters)
111+
112+
def _get_request_location(self, spec_location):
113+
return self.SPEC_LOCATION_TO_REQUEST_LOCATION[spec_location]
114+
115+
def _get_raw_value(self, request, param):
116+
request_location = self._get_request_location(param.location)
117+
request_attr = getattr(request, request_location)
118+
119+
try:
120+
return request_attr[param.name]
121+
except KeyError:
122+
raise MissingParameterError(
123+
"Missing required `{0}` parameter".format(param.name))
124+
125+
def _get_raw_body(self, request):
126+
if not request.data:
127+
raise MissingBodyError("Missing required request body")
128+
129+
return request.data

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ paths:
7373
tags:
7474
- pets
7575
requestBody:
76+
required: true
7677
content:
7778
application/json:
7879
schema:
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import json
2+
import pytest
3+
4+
from openapi_core.exceptions import (
5+
InvalidServerError, InvalidOperationError, MissingParameterError,
6+
MissingBodyError, InvalidContentTypeError,
7+
)
8+
from openapi_core.shortcuts import create_spec
9+
from openapi_core.validators import RequestValidator
10+
from openapi_core.wrappers import BaseOpenAPIRequest
11+
12+
13+
class RequestMock(BaseOpenAPIRequest):
14+
15+
def __init__(
16+
self, host_url, method, path, path_pattern=None, args=None,
17+
view_args=None, headers=None, cookies=None, data=None,
18+
mimetype='application/json'):
19+
self.host_url = host_url
20+
self.path = path
21+
self.path_pattern = path_pattern or path
22+
self.method = method
23+
24+
self.args = args or {}
25+
self.view_args = view_args or {}
26+
self.headers = headers or {}
27+
self.cookies = cookies or {}
28+
self.data = data or ''
29+
30+
self.mimetype = mimetype
31+
32+
33+
class TestRequestValidator(object):
34+
35+
host_url = 'http://petstore.swagger.io'
36+
37+
@pytest.fixture
38+
def spec_dict(self, factory):
39+
return factory.spec_from_file("data/v3.0/petstore.yaml")
40+
41+
@pytest.fixture
42+
def spec(self, spec_dict):
43+
return create_spec(spec_dict)
44+
45+
@pytest.fixture
46+
def validator(self, spec):
47+
return RequestValidator(spec)
48+
49+
def test_request_server_error(self, validator):
50+
request = RequestMock('http://petstore.invalid.net/v1', 'get', '/')
51+
52+
result = validator.validate(request)
53+
54+
assert len(result.errors) == 1
55+
assert type(result.errors[0]) == InvalidServerError
56+
assert result.body is None
57+
assert result.parameters == {}
58+
59+
def test_invalid_operation(self, validator):
60+
request = RequestMock(self.host_url, 'get', '/v1')
61+
62+
result = validator.validate(request)
63+
64+
assert len(result.errors) == 1
65+
assert type(result.errors[0]) == InvalidOperationError
66+
assert result.body is None
67+
assert result.parameters == {}
68+
69+
def test_missing_parameter(self, validator):
70+
request = RequestMock(self.host_url, 'get', '/v1/pets')
71+
72+
result = validator.validate(request)
73+
74+
assert type(result.errors[0]) == MissingParameterError
75+
assert result.body is None
76+
assert result.parameters == {
77+
'query': {
78+
'page': 1,
79+
'search': '',
80+
},
81+
}
82+
83+
def test_get_pets(self, validator):
84+
request = RequestMock(
85+
self.host_url, 'get', '/v1/pets',
86+
path_pattern='/v1/pets', args={'limit': '10'},
87+
)
88+
89+
result = validator.validate(request)
90+
91+
assert result.errors == []
92+
assert result.body is None
93+
assert result.parameters == {
94+
'query': {
95+
'limit': 10,
96+
'page': 1,
97+
'search': '',
98+
},
99+
}
100+
101+
def test_missing_body(self, validator):
102+
request = RequestMock(
103+
self.host_url, 'post', '/v1/pets',
104+
path_pattern='/v1/pets',
105+
)
106+
107+
result = validator.validate(request)
108+
109+
assert len(result.errors) == 1
110+
assert type(result.errors[0]) == MissingBodyError
111+
assert result.body is None
112+
assert result.parameters == {}
113+
114+
def test_invalid_content_type(self, validator):
115+
request = RequestMock(
116+
self.host_url, 'post', '/v1/pets',
117+
path_pattern='/v1/pets', mimetype='text/csv',
118+
)
119+
120+
result = validator.validate(request)
121+
122+
assert len(result.errors) == 1
123+
assert type(result.errors[0]) == InvalidContentTypeError
124+
assert result.body is None
125+
assert result.parameters == {}
126+
127+
def test_post_pets(self, validator, spec_dict):
128+
pet_name = 'Cat'
129+
pet_tag = 'cats'
130+
pet_street = 'Piekna'
131+
pet_city = 'Warsaw'
132+
data_json = {
133+
'name': pet_name,
134+
'tag': pet_tag,
135+
'position': '2',
136+
'address': {
137+
'street': pet_street,
138+
'city': pet_city,
139+
}
140+
}
141+
data = json.dumps(data_json)
142+
request = RequestMock(
143+
self.host_url, 'post', '/v1/pets',
144+
path_pattern='/v1/pets', data=data,
145+
)
146+
147+
result = validator.validate(request)
148+
149+
assert result.errors == []
150+
assert result.parameters == {}
151+
152+
schemas = spec_dict['components']['schemas']
153+
pet_model = schemas['PetCreate']['x-model']
154+
address_model = schemas['Address']['x-model']
155+
assert result.body.__class__.__name__ == pet_model
156+
assert result.body.name == pet_name
157+
assert result.body.tag == pet_tag
158+
assert result.body.position == 2
159+
assert result.body.address.__class__.__name__ == address_model
160+
assert result.body.address.street == pet_street
161+
assert result.body.address.city == pet_city
162+
163+
def test_get_pet(self, validator):
164+
request = RequestMock(
165+
self.host_url, 'get', '/v1/pets/1',
166+
path_pattern='/v1/pets/{petId}', view_args={'petId': '1'},
167+
)
168+
169+
result = validator.validate(request)
170+
171+
assert result.errors == []
172+
assert result.body is None
173+
assert result.parameters == {
174+
'path': {
175+
'petId': 1,
176+
},
177+
}

0 commit comments

Comments
 (0)