Skip to content

Commit 7d72e74

Browse files
committed
Added default values handling for json schema validators
1 parent beb3cef commit 7d72e74

File tree

4 files changed

+319
-30
lines changed

4 files changed

+319
-30
lines changed

aiohttp_swagger/helpers/validation.py

Lines changed: 55 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@
2121
Response,
2222
json_response,
2323
)
24-
from collections import defaultdict
24+
from collections import defaultdict, MutableMapping
2525
from jsonschema import (
26-
validate,
2726
ValidationError,
2827
FormatChecker,
28+
Draft4Validator,
29+
validators,
2930
)
30-
from jsonschema.validators import validator_for
3131

3232

3333
__all__ = (
@@ -54,12 +54,35 @@ def multi_dict_to_dict(mld: Mapping) -> Mapping:
5454
}
5555

5656

57-
def validate_schema(obj: Mapping, schema: Mapping):
58-
validate(obj, schema, format_checker=FormatChecker())
57+
def extend_with_default(validator_class):
5958

59+
validate_properties = validator_class.VALIDATORS["properties"]
6060

61-
def validate_multi_dict(obj, schema):
62-
validate(multi_dict_to_dict(obj), schema, format_checker=FormatChecker())
61+
def set_defaults(validator, properties, instance, schema):
62+
if isinstance(instance, MutableMapping):
63+
for prop, sub_schema in properties.items():
64+
if "default" in sub_schema:
65+
instance.setdefault(prop, sub_schema["default"])
66+
for error in validate_properties(
67+
validator, properties, instance, schema):
68+
yield error
69+
70+
return validators.extend(validator_class, {"properties": set_defaults})
71+
72+
73+
json_schema_validator = extend_with_default(Draft4Validator)
74+
75+
76+
def validate_schema(obj: Mapping, schema: Mapping) -> Mapping:
77+
json_schema_validator(schema, format_checker=FormatChecker()).validate(obj)
78+
return obj
79+
80+
81+
def validate_multi_dict(obj, schema) -> Mapping:
82+
_obj = multi_dict_to_dict(obj)
83+
json_schema_validator(
84+
schema, format_checker=FormatChecker()).validate(_obj)
85+
return _obj
6386

6487

6588
def validate_content_type(swagger: Mapping, content_type: str):
@@ -73,34 +96,34 @@ async def validate_request(
7396
request: Request,
7497
parameter_groups: Mapping,
7598
swagger: Mapping):
99+
res = {}
76100
validate_content_type(swagger, request.content_type)
77-
for group_name, group_schemas in parameter_groups.items():
101+
for group_name, group_schema in parameter_groups.items():
78102
if group_name == 'header':
79-
headers = request.headers
80-
for schema in group_schemas:
81-
validate_multi_dict(headers, schema)
103+
res['headers'] = validate_multi_dict(request.headers, group_schema)
82104
if group_name == 'query':
83-
query = request.query
84-
for schema in group_schemas:
85-
validate_multi_dict(query, schema)
105+
res['query'] = validate_multi_dict(request.query, group_schema)
86106
if group_name == 'formData':
87107
try:
88108
data = await request.post()
89109
except ValueError:
90110
data = None
91-
for schema in group_schemas:
92-
validate_multi_dict(data, schema)
111+
res['formData'] = validate_multi_dict(data, group_schema)
93112
if group_name == 'body':
94-
try:
95-
content = await request.json()
96-
except json.JSONDecodeError:
97-
content = None
98-
for schema in group_schemas:
99-
validate_schema(content, schema)
113+
if request.content_type == 'application/json':
114+
try:
115+
content = await request.json()
116+
except json.JSONDecodeError:
117+
content = None
118+
elif request.content_type.startswith('text'):
119+
content = await request.text()
120+
else:
121+
content = await request.read()
122+
res['body'] = validate_schema(content, group_schema)
100123
if group_name == 'path':
101124
params = dict(request.match_info)
102-
for schema in group_schemas:
103-
validate_schema(params, schema)
125+
res['path'] = validate_schema(params, group_schema)
126+
return res
104127

105128

106129
def adjust_swagger_item_to_json_schemes(*schemes: Mapping) -> Mapping:
@@ -124,7 +147,7 @@ def adjust_swagger_item_to_json_schemes(*schemes: Mapping) -> Mapping:
124147
required_fields.append(name)
125148
if required_fields:
126149
new_schema['required'] = required_fields
127-
validator_for(new_schema).check_schema(new_schema)
150+
validators.validator_for(new_schema).check_schema(new_schema)
128151
return new_schema
129152

130153

@@ -139,21 +162,21 @@ def adjust_swagger_body_item_to_json_schema(schema: Mapping) -> Mapping:
139162
new_schema,
140163
]
141164
}
142-
validator_for(new_schema).check_schema(new_schema)
165+
validators.validator_for(new_schema).check_schema(new_schema)
143166
return new_schema
144167

145168

146169
def adjust_swagger_to_json_schema(parameter_groups: Iterable) -> Mapping:
147-
res = defaultdict(list)
170+
res = {}
148171
for group_name, group_schemas in parameter_groups:
149172
if group_name in ('query', 'header', 'path', 'formData'):
150173
json_schema = adjust_swagger_item_to_json_schemes(*group_schemas)
151-
res[group_name].append(json_schema)
174+
res[group_name] = json_schema
152175
else:
153176
# only one possible schema for in: body
154177
schema = list(group_schemas)[0]
155178
json_schema = adjust_swagger_body_item_to_json_schema(schema)
156-
res[group_name].append(json_schema)
179+
res[group_name] = json_schema
157180
return res
158181

159182

@@ -216,7 +239,9 @@ async def _wrapper(*args, **kwargs) -> Response:
216239
request = args[0].request \
217240
if isinstance(args[0], web.View) else args[0]
218241
try:
219-
await validate_request(request, parameter_groups, schema)
242+
validation = \
243+
await validate_request(request, parameter_groups, schema)
244+
request.validation = validation
220245
except ValidationError as exc:
221246
logger.exception(exc)
222247
exc_dict = validation_exc_to_dict(exc)

doc/source/customizing.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,17 @@ Global Swagger YAML
212212
213213
214214
:samp:`aiohttp-swagger` also allow to validate swagger schema against json schema:
215+
Validated object would be added as **request.validation**. Default values also will be filled into object.
216+
217+
.. code-block:: javascript
218+
219+
{
220+
'query': {}, // validated request.query
221+
'path': {}, // validated request.path
222+
'body': {}, // validated request.json()
223+
'formData': {}, // validated post request.data()
224+
'headers': {}, // validated post request.headers
225+
}
215226
216227
.. code-block:: python
217228

tests/test_validation_body.py

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import asyncio
2+
import json
3+
4+
import pytest
5+
from aiohttp import web
6+
from aiohttp_swagger import *
7+
8+
9+
@asyncio.coroutine
10+
@swagger_validation
11+
def post1(request, *args, **kwargs):
12+
"""
13+
---
14+
description: Post resources
15+
tags:
16+
- Function View
17+
produces:
18+
- application/json
19+
consumes:
20+
- application/json
21+
parameters:
22+
- in: body
23+
name: body
24+
required: true
25+
schema:
26+
type: object
27+
properties:
28+
test:
29+
type: string
30+
default: default
31+
minLength: 2
32+
test1:
33+
type: string
34+
default: default1
35+
minLength: 2
36+
responses:
37+
"200":
38+
description: successful operation.
39+
"405":
40+
description: invalid HTTP Method
41+
"""
42+
return web.json_response(data=request.validation['body'])
43+
44+
45+
@asyncio.coroutine
46+
@swagger_validation
47+
def post2(request, *args, **kwargs):
48+
"""
49+
---
50+
description: Post resources
51+
tags:
52+
- Function View
53+
produces:
54+
- text/plain
55+
consumes:
56+
- text/plain
57+
parameters:
58+
- in: body
59+
name: body
60+
required: true
61+
schema:
62+
type: string
63+
default: default
64+
minLength: 2
65+
responses:
66+
"200":
67+
description: successful operation.
68+
"405":
69+
description: invalid HTTP Method
70+
"""
71+
return web.Response(text=request.validation['body'])
72+
73+
74+
POST1_METHOD_PARAMETERS = [
75+
# success
76+
(
77+
'post',
78+
'/example12',
79+
{'test': 'default'},
80+
{'Content-Type': 'application/json'},
81+
200
82+
),
83+
# success
84+
(
85+
'post',
86+
'/example12',
87+
{},
88+
{'Content-Type': 'application/json'},
89+
200
90+
),
91+
# error
92+
(
93+
'post',
94+
'/example12',
95+
None,
96+
{'Content-Type': 'application/json'},
97+
400
98+
),
99+
]
100+
101+
POST2_METHOD_PARAMETERS = [
102+
# success
103+
(
104+
'post',
105+
'/example12',
106+
'1234',
107+
{'Content-Type': 'text/plain'},
108+
200
109+
),
110+
(
111+
'post',
112+
'/example12',
113+
None,
114+
{'Content-Type': 'text/plain'},
115+
400
116+
),
117+
]
118+
119+
120+
@pytest.mark.parametrize("method,url,body,headers,response",
121+
POST1_METHOD_PARAMETERS)
122+
@asyncio.coroutine
123+
def test_function_post1_method_body_validation(
124+
test_client, loop, swagger_file, method, url, body, headers, response):
125+
app = web.Application(loop=loop)
126+
app.router.add_post("/example12", post1)
127+
setup_swagger(
128+
app,
129+
swagger_merge_with_file=True,
130+
swagger_validate_schema=True,
131+
swagger_from_file=swagger_file,
132+
)
133+
client = yield from test_client(app)
134+
data = json.dumps(body) \
135+
if headers['Content-Type'] == 'application/json' else body
136+
resp = yield from getattr(client, method)(url, data=data, headers=headers)
137+
text = yield from resp.json()
138+
assert resp.status == response, text
139+
if response != 200:
140+
assert 'error' in text
141+
else:
142+
assert 'error' not in text
143+
assert 'test' in text
144+
assert text['test'] == 'default'
145+
assert text['test1'] == 'default1'
146+
147+
148+
@pytest.mark.parametrize("method,url,body,headers,response",
149+
POST2_METHOD_PARAMETERS)
150+
@asyncio.coroutine
151+
def test_function_post2_method_body_validation(
152+
test_client, loop, swagger_file, method, url, body, headers, response):
153+
app = web.Application(loop=loop)
154+
app.router.add_post("/example12", post2)
155+
setup_swagger(
156+
app,
157+
swagger_merge_with_file=True,
158+
swagger_validate_schema=True,
159+
swagger_from_file=swagger_file,
160+
)
161+
client = yield from test_client(app)
162+
data = json.dumps(body) \
163+
if headers['Content-Type'] == 'application/json' else body
164+
resp = yield from getattr(client, method)(url, data=data, headers=headers)
165+
text = yield from resp.text()
166+
assert resp.status == response, text
167+
if response != 200:
168+
assert 'error' in text
169+
else:
170+
assert isinstance(text, str)

0 commit comments

Comments
 (0)