Skip to content

Commit 762d14a

Browse files
committed
add jsonschema support and top level input validation
1 parent d3f1db3 commit 762d14a

File tree

5 files changed

+110
-11
lines changed

5 files changed

+110
-11
lines changed

.travis.yml

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ python:
44
# command to install dependencies
55
install:
66
- pip install -r requirements.txt
7+
- pip install jsonschema
78
- pip install coveralls
89
# command to run tests
910
script: nosetests --with-coverage --cover-package=flask_inputs

README.md

+28
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,34 @@ def api():
5656

5757
If `validate()` returns `False`, error messages are listed in `inputs.errors`
5858

59+
Alternatively, to validate an entire input (as opposed to single fields like above), validators can be attached directly:
60+
61+
```
62+
class ApiInputs(Inputs):
63+
args = [CustomArgValidator()]
64+
```
65+
66+
In this case, the raw input data is passed to the validators.
67+
68+
### JSON Validation
69+
70+
Flask-Inputs provides a validator for JSON data. This requires [jsonschema](https://pypi.python.org/pypi/jsonschema).
71+
72+
```
73+
from flask_inputs.validators import JsonSchema
74+
75+
schema = {
76+
'type': 'object',
77+
'properties': {
78+
'name': {'type': 'string'}
79+
}
80+
}
81+
82+
class JsonInputs(Inputs):
83+
json = [JsonSchema(schema=schema)]
84+
```
85+
86+
5987
### Goals
6088

6189
#### Validate incoming request data

flask_inputs/inputs.py

+21-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11

2+
import collections
23
from itertools import chain
34

45
from werkzeug.datastructures import MultiDict
@@ -8,8 +9,8 @@
89

910

1011
class Inputs(object):
11-
valid_attributes = ['args', 'form', 'values', 'cookies',
12-
'headers', 'json', 'rule']
12+
_valid_attributes = ['args', 'form', 'values', 'cookies',
13+
'headers', 'json', 'rule']
1314

1415
def __init__(self, request):
1516
"""
@@ -30,28 +31,34 @@ class TellInputs(Inputs):
3031
self._forms = dict()
3132

3233
for name in dir(self):
33-
if not name.startswith('_'):
34+
if not name.startswith('_') and name not in ['errors', 'validate']:
3435
input = getattr(self, name)
36+
fields = dict()
3537

3638
if isinstance(input, dict):
37-
fields = dict()
38-
3939
for field, validators in input.iteritems():
4040
fields[field] = Field(validators=validators)
41+
elif isinstance(input, collections.Iterable):
42+
fields['_input'] = Field(validators=input)
4143

42-
self._forms[name] = BaseForm(fields)
44+
self._forms[name] = BaseForm(fields)
4345

44-
def _get_values(self, attribute):
46+
def _get_values(self, attribute, coerse=True):
4547
"""
4648
:param attribute: Request attribute to return values for.
4749
4850
Returns a MultiDict for compatibility with wtforms form data.
4951
"""
50-
if attribute in self.valid_attributes:
52+
if attribute in self._valid_attributes:
5153
if attribute == 'rule':
52-
return MultiDict(self._request.view_args)
54+
ret = self._request.view_args
55+
else:
56+
ret = getattr(self._request, attribute)
5357

54-
return MultiDict(getattr(self._request, attribute))
58+
if coerse:
59+
return MultiDict(ret)
60+
else:
61+
return MultiDict(dict(_input=ret))
5562

5663
def validate(self):
5764
"""
@@ -60,7 +67,10 @@ def validate(self):
6067
success = True
6168

6269
for attribute, form in self._forms.iteritems():
63-
form.process(self._get_values(attribute))
70+
if '_input' in form._fields:
71+
form.process(self._get_values(attribute, coerse=False))
72+
else:
73+
form.process(self._get_values(attribute))
6474

6575
if not form.validate():
6676
success = False

flask_inputs/validators.py

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
2+
import jsonschema
3+
from wtforms.validators import ValidationError
4+
5+
6+
class JsonSchema(object):
7+
def __init__(self, schema, message=None):
8+
self.schema = schema
9+
self.message = message
10+
11+
def __call__(self, form, field):
12+
try:
13+
jsonschema.validate(field.data, self.schema)
14+
except jsonschema.ValidationError as e:
15+
raise ValidationError(e.message)

tests/test_jsonschema.py

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
2+
import unittest
3+
4+
from flask import Flask, request
5+
from flask_inputs import Inputs
6+
from flask_inputs.validators import JsonSchema
7+
8+
9+
app = Flask(__name__)
10+
11+
schema = {
12+
'type': 'object',
13+
'properties': {
14+
'name': {'type': 'string'}
15+
}
16+
}
17+
18+
19+
class JsonInputs(Inputs):
20+
json = [JsonSchema(schema=schema)]
21+
22+
23+
valid_data = '{"name": "Nathan Cahill"}'
24+
invalid_data = '{"name": 100}'
25+
26+
27+
class JsonSchemaTest(unittest.TestCase):
28+
def test_valid(self):
29+
with app.test_request_context(method='POST', data=valid_data, content_type='application/json'):
30+
inputs = JsonInputs(request)
31+
32+
self.assertTrue(inputs.validate())
33+
34+
def test_invalid(self):
35+
with app.test_request_context(method='POST', data=invalid_data, content_type='application/json'):
36+
inputs = JsonInputs(request)
37+
38+
self.assertFalse(inputs.validate())
39+
40+
def test_error_messages(self):
41+
with app.test_request_context(method='POST', data=invalid_data, content_type='application/json'):
42+
inputs = JsonInputs(request)
43+
inputs.validate()
44+
45+
self.assertEqual(inputs.errors, ["100 is not of type 'string'"])

0 commit comments

Comments
 (0)