Repository: codingedward/flask-sieve

Issue:
validation error
Hello

I think I found a bug with `required_without` and `required_without_all` when I used it with parameter type.

example:

```
def rules(self):
        return {
            'id': ['required_without:name', integer],
            'name': ['required_without:id', 'string']
        }
```

and When the request json only has ID
```
{
   "id": 123
}
```

the error response is:
```
{
  "errors": {
    "user_id": [
      "The name must be an string."
    ]
  },
  "message": "Validation error",
  "success": false
}
```

Can You please check this issue?


Comments:

Patch:
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 PIL import Image
 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 @@ class RulesProcessor:
         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 @@ class RulesProcessor:
             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 @@ class RulesProcessor:
                 'Cannot call method %s with value %s' %
                 (method.__name__, str(value))
             )
-
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"


Test patch:
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 @@ class TestRulesProcessor(unittest.TestCase):
             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 @@ class TestRulesProcessor(unittest.TestCase):
             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 @@ class TestRulesProcessor(unittest.TestCase):
             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 @@ class TestRulesProcessor(unittest.TestCase):
             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}