mirror of
https://github.com/zulip/zulip.git
synced 2025-11-04 14:03:30 +00:00
Add validators.py (with tests).
This sets up a scheme to validate complex data structures and give specific error messages for improperly typed parameters. (imported from commit 33b2f070d993da4ee929119dd41503bd0128c8eb)
This commit is contained in:
76
zerver/lib/validator.py
Normal file
76
zerver/lib/validator.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
'''
|
||||||
|
This module sets up a scheme for validating that arbitrary Python
|
||||||
|
objects are correctly typed. It is totally decoupled from Django,
|
||||||
|
composable, easily wrapped, and easily extended.
|
||||||
|
|
||||||
|
A validator takes two parameters--var_name and val--and returns an
|
||||||
|
error if val is not the correct type. The var_name parameter is used
|
||||||
|
to format error messages. Validators return None when there are no errors.
|
||||||
|
|
||||||
|
Example primitive validators are check_string, check_int, and check_bool.
|
||||||
|
|
||||||
|
Compound validators are created by check_list and check_dict. Note that
|
||||||
|
those functions aren't directly called for validation; instead, those
|
||||||
|
functions are called to return other functions that adhere to the validator
|
||||||
|
contract. This is similar to how Python decorators are often parameterized.
|
||||||
|
|
||||||
|
The contract for check_list and check_dict is that they get passed in other
|
||||||
|
validators to apply to their items. This allows you to build up validators
|
||||||
|
for arbitrarily complex validators. See ValidatorTestCase for example usage.
|
||||||
|
|
||||||
|
A simple example of composition is this:
|
||||||
|
|
||||||
|
check_list(check_string)('my_list', ['a', 'b', 'c']) == None
|
||||||
|
|
||||||
|
To extend this concept, it's simply a matter of writing your own validator
|
||||||
|
for any particular type of object.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def check_string(var_name, val):
|
||||||
|
if not isinstance(val, basestring):
|
||||||
|
return '%s is not a string' % (var_name,)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def check_int(var_name, val):
|
||||||
|
if not isinstance(val, int):
|
||||||
|
return '%s is not an integer' % (var_name,)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def check_bool(var_name, val):
|
||||||
|
if not isinstance(val, bool):
|
||||||
|
return '%s is not a boolean' % (var_name,)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def check_list(sub_validator):
|
||||||
|
def f(var_name, val):
|
||||||
|
if not isinstance(val, list):
|
||||||
|
return '%s is not a list' % (var_name,)
|
||||||
|
|
||||||
|
for i, item in enumerate(val):
|
||||||
|
vname = '%s[%d]' % (var_name, i)
|
||||||
|
error = sub_validator(vname, item)
|
||||||
|
if error:
|
||||||
|
return error
|
||||||
|
|
||||||
|
return None
|
||||||
|
return f
|
||||||
|
|
||||||
|
def check_dict(required_keys):
|
||||||
|
# required_keys is a list of tuples of
|
||||||
|
# key_name/validator
|
||||||
|
|
||||||
|
def f(var_name, val):
|
||||||
|
if not isinstance(val, dict):
|
||||||
|
return '%s is not a dict' % (var_name,)
|
||||||
|
|
||||||
|
for k, sub_validator in required_keys:
|
||||||
|
if k not in val:
|
||||||
|
return '%s key is missing from %s' % (k, var_name)
|
||||||
|
vname = '%s["%s"]' % (var_name, k)
|
||||||
|
error = sub_validator(vname, val[k])
|
||||||
|
if error:
|
||||||
|
return error
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
return f
|
||||||
@@ -31,6 +31,8 @@ from zerver.lib.alert_words import alert_words_in_realm, user_alert_words, \
|
|||||||
add_user_alert_words, remove_user_alert_words
|
add_user_alert_words, remove_user_alert_words
|
||||||
from zerver.lib.digest import send_digest_email
|
from zerver.lib.digest import send_digest_email
|
||||||
from zerver.forms import not_mit_mailing_list
|
from zerver.forms import not_mit_mailing_list
|
||||||
|
from zerver.lib.validator import check_string, check_list, check_dict, \
|
||||||
|
check_bool, check_int
|
||||||
|
|
||||||
from zerver.worker import queue_processors
|
from zerver.worker import queue_processors
|
||||||
|
|
||||||
@@ -198,6 +200,93 @@ def is_known_slow_test(test_method):
|
|||||||
|
|
||||||
API_KEYS = {}
|
API_KEYS = {}
|
||||||
|
|
||||||
|
class ValidatorTestCase(TestCase):
|
||||||
|
def test_check_string(self):
|
||||||
|
x = "hello"
|
||||||
|
self.assertEqual(check_string('x', x), None)
|
||||||
|
|
||||||
|
x = 4
|
||||||
|
self.assertEqual(check_string('x', x), 'x is not a string')
|
||||||
|
|
||||||
|
def test_check_bool(self):
|
||||||
|
x = True
|
||||||
|
self.assertEqual(check_bool('x', x), None)
|
||||||
|
|
||||||
|
x = 4
|
||||||
|
self.assertEqual(check_bool('x', x), 'x is not a boolean')
|
||||||
|
|
||||||
|
def test_check_int(self):
|
||||||
|
x = 5
|
||||||
|
self.assertEqual(check_int('x', x), None)
|
||||||
|
|
||||||
|
x = [{}]
|
||||||
|
self.assertEqual(check_int('x', x), 'x is not an integer')
|
||||||
|
|
||||||
|
def test_check_list(self):
|
||||||
|
x = 999
|
||||||
|
error = check_list(check_string)('x', x)
|
||||||
|
self.assertEqual(error, 'x is not a list')
|
||||||
|
|
||||||
|
x = ["hello", 5]
|
||||||
|
error = check_list(check_string)('x', x)
|
||||||
|
self.assertEqual(error, 'x[1] is not a string')
|
||||||
|
|
||||||
|
x = [["yo"], ["hello", "goodbye", 5]]
|
||||||
|
error = check_list(check_list(check_string))('x', x)
|
||||||
|
self.assertEqual(error, 'x[1][2] is not a string')
|
||||||
|
|
||||||
|
def test_check_dict(self):
|
||||||
|
keys = [
|
||||||
|
('names', check_list(check_string)),
|
||||||
|
('city', check_string),
|
||||||
|
]
|
||||||
|
|
||||||
|
x = {
|
||||||
|
'names': ['alice', 'bob'],
|
||||||
|
'city': 'Boston',
|
||||||
|
}
|
||||||
|
error = check_dict(keys)('x', x)
|
||||||
|
self.assertEqual(error, None)
|
||||||
|
|
||||||
|
x = 999
|
||||||
|
error = check_dict(keys)('x', x)
|
||||||
|
self.assertEqual(error, 'x is not a dict')
|
||||||
|
|
||||||
|
x = {}
|
||||||
|
error = check_dict(keys)('x', x)
|
||||||
|
self.assertEqual(error, 'names key is missing from x')
|
||||||
|
|
||||||
|
x = {
|
||||||
|
'names': ['alice', 'bob', {}]
|
||||||
|
}
|
||||||
|
error = check_dict(keys)('x', x)
|
||||||
|
self.assertEqual(error, 'x["names"][2] is not a string')
|
||||||
|
|
||||||
|
x = {
|
||||||
|
'names': ['alice', 'bob'],
|
||||||
|
'city': 5
|
||||||
|
}
|
||||||
|
error = check_dict(keys)('x', x)
|
||||||
|
self.assertEqual(error, 'x["city"] is not a string')
|
||||||
|
|
||||||
|
def test_encapsulation(self):
|
||||||
|
# There might be situations where we want deep
|
||||||
|
# validation, but the error message should be customized.
|
||||||
|
# This is an example.
|
||||||
|
def check_person(val):
|
||||||
|
error = check_dict([
|
||||||
|
['name', check_string],
|
||||||
|
['age', check_int],
|
||||||
|
])('_', val)
|
||||||
|
if error:
|
||||||
|
return 'This is not a valid person'
|
||||||
|
|
||||||
|
person = {'name': 'King Lear', 'age': 42}
|
||||||
|
self.assertEqual(check_person(person), None)
|
||||||
|
|
||||||
|
person = 'misconfigured data'
|
||||||
|
self.assertEqual(check_person(person), 'This is not a valid person')
|
||||||
|
|
||||||
class AuthedTestCase(TestCase):
|
class AuthedTestCase(TestCase):
|
||||||
# Helper because self.client.patch annoying requires you to urlencode
|
# Helper because self.client.patch annoying requires you to urlencode
|
||||||
def client_patch(self, url, info={}, **kwargs):
|
def client_patch(self, url, info={}, **kwargs):
|
||||||
|
|||||||
Reference in New Issue
Block a user