diff --git a/flask_sieve/conditional_inclusion_rules.py b/flask_sieve/conditional_inclusion_rules.py new file mode 100644 index 0000000..99001ba --- /dev/null +++ b/flask_sieve/conditional_inclusion_rules.py @@ -0,0 +1,8 @@ +conditional_inclusion_rules = [ + 'required_if', + 'required_unless', + 'required_with', + 'required_with_all', + 'required_without', + 'required_without_all', +] diff --git a/flask_sieve/rules_processor.py b/flask_sieve/rules_processor.py index c6e4f7e..d6f58b2 100644 --- a/flask_sieve/rules_processor.py +++ b/flask_sieve/rules_processor.py @@ -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): @@ -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'], @@ -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): @@ -628,4 +655,3 @@ def _assert_with_method(method, value): 'Cannot call method %s with value %s' % (method.__name__, str(value)) ) - diff --git a/tests/test_rules_processor.py b/tests/test_rules_processor.py index abbf64b..167145c 100644 --- a/tests/test_rules_processor.py +++ b/tests/test_rules_processor.py @@ -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']}, @@ -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): @@ -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( @@ -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} diff --git a/watch.sh b/watch.sh new file mode 100755 index 0000000..340fb07 --- /dev/null +++ b/watch.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +watchman-make -p "**/*.py" --run "nosetests --with-coverage --cover-package flask_sieve"