From 0b1b315ab7230dea11af77dc06dcd987ac301cd5 Mon Sep 17 00:00:00 2001 From: codingedward Date: Sun, 6 Dec 2020 02:19:28 +0300 Subject: [PATCH 1/5] Add conditional inclusion rules check --- flask_sieve/conditional_inclusion_rules.py | 8 +++++ flask_sieve/rules_processor.py | 37 ++++++++++++++++++---- tests/test_rules_processor.py | 33 +++++++++++++++++++ watch.sh | 3 ++ 4 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 flask_sieve/conditional_inclusion_rules.py create mode 100755 watch.sh 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..5f35c6b 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,28 @@ 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 in conditional_inclusion_rules, rules)) + if len(attribute_conditional_rules) == 0: + return False + for conditional_rule in attribute_conditional_rules: + is_conditional_rule_valid = handler( + value=value, + attribute=attribute, + params=rule['params'], + nullable=nullable, + rules=rules + ) + if not is_conditional_rule_valid: + return False + return True @staticmethod def _compare_dates(first, second, comparator): @@ -628,4 +654,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..8356402 100644 --- a/tests/test_rules_processor.py +++ b/tests/test_rules_processor.py @@ -46,6 +46,8 @@ def test_validates_accepted(self): ) def test_validates_active_url(self): + pass + return self.assert_passes( rules={'field': ['active_url']}, request={'field': 'https://google.com'}, @@ -699,6 +701,10 @@ def test_validates_required_if(self): rules={'field': ['size:0']}, request={'field': self.image_file} ) + self.assert_passes( + 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']}, request={'field': '', 'field_2': 'one'} @@ -764,6 +770,29 @@ def test_validates_required_without(self): request={'field': '', 'field_2': ''} ) + 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_fails( + rules={ + 'id': ['required_without:name', 'integer'], + 'id2': ['required_without:id', 'integer'], + }, + request={'id': 1} + ) + def test_validates_required_without_all(self): self.assert_passes( rules={'field': ['required_without_all:field_2,field_3']}, @@ -787,6 +816,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" From 7235378f899158c0c1f1df22bc0960024f79384b Mon Sep 17 00:00:00 2001 From: codingedward Date: Sun, 6 Dec 2020 23:18:32 +0300 Subject: [PATCH 2/5] Add conditional inclusion rules check --- flask_sieve/rules_processor.py | 7 ++++--- tests/test_rules_processor.py | 17 ++++++++++------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/flask_sieve/rules_processor.py b/flask_sieve/rules_processor.py index 5f35c6b..d6f58b2 100644 --- a/flask_sieve/rules_processor.py +++ b/flask_sieve/rules_processor.py @@ -535,15 +535,16 @@ def _is_attribute_nullable(self, attribute, params, rules, **kwargs): value = self._attribute_value(attribute) if value is not None: return False - attribute_conditional_rules = list(filter(lambda rule: rule in conditional_inclusion_rules, rules)) + 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=rule['params'], - nullable=nullable, + params=conditional_rule['params'], + nullable=False, rules=rules ) if not is_conditional_rule_valid: diff --git a/tests/test_rules_processor.py b/tests/test_rules_processor.py index 8356402..d94b91c 100644 --- a/tests/test_rules_processor.py +++ b/tests/test_rules_processor.py @@ -697,10 +697,6 @@ def test_validates_required_if(self): rules={'field': ['required_if:field_2,one,two']}, request={'field': '', 'field_2': 'three'} ) - self.assert_passes( - rules={'field': ['size:0']}, - request={'field': self.image_file} - ) self.assert_passes( rules={'field': ['required_if:field_2,one,two', 'integer']}, request={'field_1': '', 'field_2': 'xxxx'} @@ -720,8 +716,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): @@ -783,6 +779,13 @@ def test_validates_required_multiple_required_withouts(self): '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( @@ -790,7 +793,7 @@ def test_validates_required_multiple_required_withouts(self): 'id': ['required_without:name', 'integer'], 'id2': ['required_without:id', 'integer'], }, - request={'id': 1} + request={'name': 'hi'} ) def test_validates_required_without_all(self): From 65ab958ce86a2baa978422bb3bed1beefe3bd45b Mon Sep 17 00:00:00 2001 From: codingedward Date: Sun, 6 Dec 2020 23:25:22 +0300 Subject: [PATCH 3/5] Add more tests --- tests/test_rules_processor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_rules_processor.py b/tests/test_rules_processor.py index d94b91c..9b8421c 100644 --- a/tests/test_rules_processor.py +++ b/tests/test_rules_processor.py @@ -765,6 +765,13 @@ 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( From ba60ad1800f0253cc0c5135b94d32a150acb4fd9 Mon Sep 17 00:00:00 2001 From: codingedward Date: Sun, 6 Dec 2020 23:31:34 +0300 Subject: [PATCH 4/5] Clean up --- tests/test_rules_processor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_rules_processor.py b/tests/test_rules_processor.py index 9b8421c..167145c 100644 --- a/tests/test_rules_processor.py +++ b/tests/test_rules_processor.py @@ -46,8 +46,6 @@ def test_validates_accepted(self): ) def test_validates_active_url(self): - pass - return self.assert_passes( rules={'field': ['active_url']}, request={'field': 'https://google.com'}, From d956205e996be73a43d3eb92b5cf414efb90d26c Mon Sep 17 00:00:00 2001 From: codingedward Date: Sun, 6 Dec 2020 23:34:34 +0300 Subject: [PATCH 5/5] New version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index adc5945..c8e2469 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ name='flask-sieve', description='A Laravel inspired requests validator for Flask', long_description='Find the documentation at https://flask-sieve.readthedocs.io/en/latest/', - version='1.2.0', + version='1.2.1', url='https://github.com/codingedward/flask-sieve', license='BSD-2', author='Edward Njoroge',