Skip to content

Commit

Permalink
Merge pull request #84 from d-ganchar/bug/82
Browse files Browse the repository at this point in the history
#82 required JsonParams
  • Loading branch information
d-ganchar authored Jul 25, 2023
2 parents 449b464 + 110e186 commit 7b7a431
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 145 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,7 @@ ENV/
# Rope project settings
.ropeproject
.idea/

# debug files
debug.py
app.py
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
language: python
dist: focal
python:
- "3.6"
- "3.7"
- "3.8"
- "3.9"
Expand Down
8 changes: 8 additions & 0 deletions flask_request_validator/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ def __init__(self, errors: List[Any]) -> None:
self.errors = errors


class MissingJsonKeyError(RuleError):
def __init__(self, key: str) -> None:
self.key = key

def __str__(self) -> str:
return 'key is required'


class RulesError(RequestError):
def __init__(self, *args: RuleError):
self.errors = args
Expand Down
40 changes: 31 additions & 9 deletions flask_request_validator/nested_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
JsonError,
RequiredJsonKeyError,
JsonListItemTypeError,
RulesError, JsonListExpectedError, JsonDictExpectedError,
RulesError,
JsonListExpectedError,
JsonDictExpectedError,
MissingJsonKeyError,
)
from .rules import CompositeRule, AbstractRule

Expand Down Expand Up @@ -46,6 +49,17 @@ def _check_list_item_type(self, nested: 'JsonParam', value: Any):
if isinstance(nested.rules_map, dict) and not isinstance(value, dict):
raise JsonListItemTypeError()

def _is_missing_json_key(self, key: str, value: Dict, nested: 'JsonParam'):
rules = nested.rules_map.get(key)
if isinstance(rules, JsonParam) and not rules.required:
return

try:
if key not in value:
raise MissingJsonKeyError(key)
except MissingJsonKeyError as error:
raise RulesError(error)

def _validate_list(
self,
value: Union[Dict, List],
Expand Down Expand Up @@ -101,18 +115,26 @@ def _validate_dict(
errors: List[JsonError],
) -> Tuple[Any, List[JsonError], Dict[str, RulesError]]:
err = dict()
for key, rules in nested.rules_map.items():
if key not in value:
continue
elif isinstance(rules, JsonParam):
new_val, errors = self.validate(value[key], rules, depth + [key], errors)
continue

for key, rules in nested.rules_map.items():
try:
new_val = rules.validate(value[key])
value[key] = new_val
self._is_missing_json_key(key, value, nested)
except RulesError as e:
err[key] = e
continue

key_value = value.get(key)
if isinstance(rules, JsonParam):
if key_value is None and not nested.rules_map[key].required:
continue

new_val, errors = self.validate(key_value, rules, depth + [key], errors)
else:
try:
new_val = rules.validate(key_value)
value[key] = new_val
except RulesError as e:
err[key] = e

return value, errors, err

Expand Down
3 changes: 1 addition & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

setup(
name='flask_request_validator',
version='4.2.1',
version='4.2.2',
description='Flask request data validation',
long_description=long_description,
url='https://github.com/d-ganchar/flask_request_validator',
Expand All @@ -19,7 +19,6 @@
classifiers=[
'Development Status :: 5 - Production/Stable',
'Framework :: Flask',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
Expand Down
170 changes: 51 additions & 119 deletions tests/test_nested_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

from parameterized import parameterized

from flask_request_validator.exceptions import *
from flask_request_validator import JsonParam, Enum, CompositeRule, Min, Max, IsEmail, Number, MinLength
from flask_request_validator.exceptions import *


class TestJsonParam(unittest.TestCase):
Expand Down Expand Up @@ -59,62 +59,14 @@ class TestJsonParam(unittest.TestCase):
'meta': {'buildings': {'warehouses': {'small': {'count': 100, }, 'large': 0, }, }, },
},
[
[
['root', 'meta', 'buildings', 'warehouses', 'small'],
{'count': [ValueMaxError]},
],
[
['root', 'meta', 'buildings', 'warehouses'],
{'large': [ValueMinError]},
],
[
['root', 'meta'],
{'description': RequiredJsonKeyError},
],
[
['root'],
{'street': [ValueEnumError], },
],
"(['root', 'meta', 'buildings', 'warehouses', 'small'],"
" {'count': RulesError(ValueMaxError(99, True))}, False)",
"(['root', 'meta', 'buildings', 'warehouses'],"
" {'large': RulesError(ValueMinError(1, True))}, False)",
"(['root', 'meta'], {'description': RulesError(MissingJsonKeyError('description'))}, False)",
"(['root'], {'street': RulesError(ValueEnumError(('Jakuba Kolasa',)))}, False)",
],
),
# valid
(
DICT_SCHEMA,
{
'country': 'Belarus',
'city': 'Minsk',
'street': 'Jakuba Kolasa',
'meta': {
'buildings': {
'warehouses': {
'small': {'count': 99, },
'large': 1,
},
},
'description': {
'color': 'green',
},
},
},
{},
)
])
def test_dict(self, param: JsonParam, data, exp):
value, errors = param.validate(data)
for ix, json_error in enumerate(errors): # type: list, JsonError
self.assertTrue(isinstance(json_error, JsonError))
exp_depth, epx_errors_map = exp[ix] # type: list, dict
self.assertListEqual(json_error.depth, exp_depth)
for key, error in json_error.errors.items():
if isinstance(error, RulesError):
self.assertEqual(len(error.errors), len(epx_errors_map))
for ix_rule, rule_err in enumerate(error.errors):
self.assertTrue(isinstance(rule_err, epx_errors_map[key][0]))
else:
self.assertTrue(isinstance(error, epx_errors_map[key]))

@parameterized.expand([
# invalid
(
LIST_SCHEMA,
{
Expand All @@ -138,31 +90,39 @@ def test_dict(self, param: JsonParam, data, exp):
},
},
[
[
['root', 'person', 'info', 'contacts', 'phones'],
{
2: JsonListItemTypeError,
3: JsonListItemTypeError,
},
],
[
['root', 'person', 'info', 'contacts', 'networks'],
{
1: {'name': [ValueEnumError], },
2: {'name': [ValueEnumError], },
},
],
[
['root', 'person', 'info', 'contacts', 'emails'],
{
0: JsonListItemTypeError,
1: JsonListItemTypeError,
2: [ValueEmailError],
},
],
"(['root', 'person', 'info', 'contacts', 'phones'], "
"{2: JsonListItemTypeError(False), 3: JsonListItemTypeError(False)}, True)",
"(['root', 'person', 'info', 'contacts', 'networks'], "
"{1: {'name': RulesError(ValueEnumError(('facebook', 'telegram')))}, "
"2: {'name': RulesError(ValueEnumError(('facebook', 'telegram')))}}, True)",
"(['root', 'person', 'info', 'contacts', 'emails'], "
"{0: JsonListItemTypeError(False), 1: JsonListItemTypeError(False), "
"2: RulesError(ValueEmailError())}, True)",
],
),
# valid
(
DICT_SCHEMA,
{
'country': 'Belarus',
'city': 'Minsk',
'street': 'Jakuba Kolasa',
'meta': {
'buildings': {
'warehouses': {
'small': {'count': 99, },
'large': 1,
},
},
'description': {
'color': 'green',
},
},
},
{},
),
(
LIST_SCHEMA,
{
Expand All @@ -184,26 +144,13 @@ def test_dict(self, param: JsonParam, data, exp):
[],
),
])
def test_list(self, param: JsonParam, data, exp):
value, errors = param.validate(data)
self.assertEqual(len(exp), len(errors))
for err_ix, json_er in enumerate(errors): # type: int, JsonError
exp_err = exp[err_ix]
exp_rule_err = exp_err[1]
self.assertListEqual(json_er.depth, exp_err[0])
self.assertEqual(len(exp_err[1]), len(json_er.errors))
for rules_ix, rules_err in exp_rule_err.items():
json_rules = json_er.errors[rules_ix] # type: dict or list or RulesError
if isinstance(exp_rule_err[rules_ix], list):
self.assertTrue(isinstance(json_rules, RulesError))
for k, rule_err in enumerate(json_rules.errors):
self.assertTrue(isinstance(rule_err, exp_rule_err[rules_ix][k]))
else:
if isinstance(rules_err, dict):
self.assertTrue(len(json_rules), len(rules_err))
else:
# RulesError
self.assertTrue(isinstance(json_er.errors[rules_ix], rules_err))
def test_validate(self, param: JsonParam, data, exp):
value, errors = param.validate(deepcopy(data))
self.assertEqual(len(errors), len(exp))

for ix, json_error in enumerate(exp): # type: int, List[list, dict]
str_error = str(errors[ix])
self.assertEqual(json_error, str_error)

@parameterized.expand([
(
Expand Down Expand Up @@ -245,28 +192,13 @@ def test_root_list_invalid(self):
{'age': 15, 'name': 'good'},
])

self.assertEqual(1, len(errors))
self.assertTrue(isinstance(errors[0], JsonError))
error = errors[0]
self.assertListEqual(['root'], error.depth)
self.assertTrue(2, len(error.errors))
self.assertTrue(error.errors[0], 1)
self.assertTrue(error.errors[1], 2)
self.assertEqual(
"[JsonError(['root'], "
"{0: {'age': RulesError(NumberError())}, 1: "
"{'age': RulesError(NumberError()), 'name': RulesError(ValueMinLengthError(1))}}, True)]",
str(errors)
)

self.assertTrue(isinstance(error.errors[0]['age'].errors[0], NumberError))
self.assertTrue(isinstance(error.errors[1]['age'].errors[0], NumberError))
self.assertTrue(isinstance(error.errors[1]['name'].errors[0], ValueMinLengthError))
# invalid type - dict instead list
_, errors = param.validate({'age': 18, 'name': 'test'})
self.assertEqual(1, len(errors))
self.assertListEqual(['root'], errors[0].depth)
self.assertTrue(isinstance(errors[0], JsonListExpectedError))
# invalid type - nested string instead list
_, errors = param.validate([
{'age': 27, 'name': 'test'},
{'age': 15, 'name': 'good', 'tags': 'bad_type'},
])

self.assertEqual(1, len(errors))
self.assertListEqual(['root', 'tags'], errors[0].depth)
self.assertTrue(isinstance(errors[0], JsonListExpectedError))
self.assertEqual("[JsonListExpectedError(['root'])]", str(errors))
Loading

0 comments on commit 7b7a431

Please sign in to comment.