Skip to content

Commit

Permalink
Add conditional inclusion rules check (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
codingedward authored Dec 6, 2020
2 parents 2a8e285 + d956205 commit b0e985e
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 10 deletions.
8 changes: 8 additions & 0 deletions flask_sieve/conditional_inclusion_rules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
conditional_inclusion_rules = [
'required_if',
'required_unless',
'required_with',
'required_with_all',
'required_without',
'required_without_all',
]
38 changes: 32 additions & 6 deletions flask_sieve/rules_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from dateutil.parser import parse as dateparse
from werkzeug.datastructures import FileStorage

from .conditional_inclusion_rules import conditional_inclusion_rules

class RulesProcessor:
def __init__(self, app=None, rules=None, request=None):
Expand All @@ -36,24 +37,27 @@ def passes(self):
self._attributes_validations = {}
for attribute, rules in self._rules.items():
should_bail = self._has_rule(rules, 'bail')
nullable = self._has_rule(rules, 'nullable')
validations = []
for rule in rules:
is_valid = False
handler = self._get_rule_handler(rule['name'])
value = self._attribute_value(attribute)
attr_type = self._get_type(value, rules)
is_valid = False
if value is None and nullable:
is_nullable = self._is_attribute_nullable(
attribute=attribute,
params=rule['params'],
rules=rules,
)
if value is None and is_nullable:
is_valid = True
else:
is_valid = handler(
value=value,
attribute=attribute,
params=rule['params'],
nullable=nullable,
nullable=is_nullable,
rules=rules
)

validations.append({
'attribute': attribute,
'rule': rule['name'],
Expand Down Expand Up @@ -523,6 +527,29 @@ def validate_uuid(value, **kwargs):
r'^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$',
str(value).lower()
) is not None

def _is_attribute_nullable(self, attribute, params, rules, **kwargs):
is_explicitly_nullable = self._has_rule(rules, 'nullable')
if is_explicitly_nullable:
return True
value = self._attribute_value(attribute)
if value is not None:
return False
attribute_conditional_rules = list(filter(lambda rule: rule['name'] in conditional_inclusion_rules, rules))
if len(attribute_conditional_rules) == 0:
return False
for conditional_rule in attribute_conditional_rules:
handler = self._get_rule_handler(conditional_rule['name'])
is_conditional_rule_valid = handler(
value=value,
attribute=attribute,
params=conditional_rule['params'],
nullable=False,
rules=rules
)
if not is_conditional_rule_valid:
return False
return True

@staticmethod
def _compare_dates(first, second, comparator):
Expand Down Expand Up @@ -628,4 +655,3 @@ def _assert_with_method(method, value):
'Cannot call method %s with value %s' %
(method.__name__, str(value))
)

49 changes: 45 additions & 4 deletions tests/test_rules_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -696,8 +696,8 @@ def test_validates_required_if(self):
request={'field': '', 'field_2': 'three'}
)
self.assert_passes(
rules={'field': ['size:0']},
request={'field': self.image_file}
rules={'field': ['required_if:field_2,one,two', 'integer']},
request={'field_1': '', 'field_2': 'xxxx'}
)
self.assert_fails(
rules={'field': ['required_if:field_2,one,two']},
Expand All @@ -714,8 +714,8 @@ def test_validates_required_unless(self):
request={'field': '', 'field_2': 'one'}
)
self.assert_fails(
rules={'field': ['required_unless:field_2,one,two']},
request={'field': '', 'field_2': 'three'}
rules={'field': ['required_unless:field_2,one,two', 'string']},
request={'field_2': 'three'}
)

def test_validates_required_with(self):
Expand Down Expand Up @@ -763,6 +763,43 @@ def test_validates_required_without(self):
rules={'field': ['required_without:field_2,field_3']},
request={'field': '', 'field_2': ''}
)
self.assert_passes(
rules={
'id': ['required_without:name', 'integer'],
'name': ['required_without:id', 'string', 'confirmed']
},
request={'id': 123}
)

def test_validates_required_multiple_required_withouts(self):
self.assert_passes(
rules={
'id': ['required_without:name', 'integer'],
'name': ['required_without:id', 'string'],
},
request={'id': 1, 'name': ''}
)
self.assert_passes(
rules={
'id': ['required_without:name', 'integer'],
'name': ['required_without:id', 'string', 'nullable'],
},
request={'id': 1},
)
self.assert_passes(
rules={
'id': ['required_without:name', 'integer'],
'id2': ['required_without:id', 'integer'],
},
request={'id': 1}
)
self.assert_fails(
rules={
'id': ['required_without:name', 'integer'],
'id2': ['required_without:id', 'integer'],
},
request={'name': 'hi'}
)

def test_validates_required_without_all(self):
self.assert_passes(
Expand All @@ -787,6 +824,10 @@ def test_validates_same(self):
rules={'field': ['same:field_2']},
request={'field': 1, 'field_2': 1}
)
self.assert_fails(
rules={'field': ['same:field_2']},
request={'field': '1', 'field_2': 1}
)
self.assert_fails(
rules={'field': ['same:field_2']},
request={'field': 1, 'field_2': 2}
Expand Down
3 changes: 3 additions & 0 deletions watch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash

watchman-make -p "**/*.py" --run "nosetests --with-coverage --cover-package flask_sieve"

0 comments on commit b0e985e

Please sign in to comment.