safeshield 1.2.2__tar.gz → 1.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of safeshield might be problematic. Click here for more details.

Files changed (45) hide show
  1. {safeshield-1.2.2 → safeshield-1.4.2}/PKG-INFO +7 -1
  2. {safeshield-1.2.2 → safeshield-1.4.2}/README.md +7 -1
  3. {safeshield-1.2.2 → safeshield-1.4.2}/safeshield.egg-info/PKG-INFO +7 -1
  4. {safeshield-1.2.2 → safeshield-1.4.2}/safeshield.egg-info/SOURCES.txt +2 -2
  5. {safeshield-1.2.2 → safeshield-1.4.2}/safeshield.egg-info/requires.txt +2 -0
  6. {safeshield-1.2.2 → safeshield-1.4.2}/setup.py +1 -1
  7. {safeshield-1.2.2 → safeshield-1.4.2}/validator/core/validator.py +10 -10
  8. {safeshield-1.2.2 → safeshield-1.4.2}/validator/factory.py +4 -4
  9. safeshield-1.4.2/validator/rules/__init__.py +24 -0
  10. {safeshield-1.2.2 → safeshield-1.4.2}/validator/rules/array.py +27 -34
  11. {safeshield-1.2.2 → safeshield-1.4.2}/validator/rules/base.py +32 -8
  12. safeshield-1.4.2/validator/rules/basic.py +136 -0
  13. safeshield-1.4.2/validator/rules/boolean.py +157 -0
  14. {safeshield-1.2.2 → safeshield-1.4.2}/validator/rules/comparison.py +130 -40
  15. {safeshield-1.2.2 → safeshield-1.4.2}/validator/rules/date.py +36 -20
  16. safeshield-1.4.2/validator/rules/files.py +279 -0
  17. safeshield-1.4.2/validator/rules/format.py +196 -0
  18. safeshield-1.4.2/validator/rules/numeric.py +188 -0
  19. safeshield-1.4.2/validator/rules/string.py +126 -0
  20. safeshield-1.4.2/validator/rules/utilities.py +313 -0
  21. {safeshield-1.2.2 → safeshield-1.4.2}/validator/services/rule_conflict.py +2 -2
  22. safeshield-1.4.2/validator/services/rule_error_handler.py +239 -0
  23. {safeshield-1.2.2 → safeshield-1.4.2}/validator/services/rule_preparer.py +12 -25
  24. safeshield-1.2.2/validator/rules/__init__.py +0 -26
  25. safeshield-1.2.2/validator/rules/basic.py +0 -41
  26. safeshield-1.2.2/validator/rules/conditional.py +0 -332
  27. safeshield-1.2.2/validator/rules/files.py +0 -167
  28. safeshield-1.2.2/validator/rules/format.py +0 -105
  29. safeshield-1.2.2/validator/rules/string.py +0 -74
  30. safeshield-1.2.2/validator/rules/type.py +0 -42
  31. safeshield-1.2.2/validator/rules/utilities.py +0 -213
  32. safeshield-1.2.2/validator/services/rule_error_handler.py +0 -42
  33. {safeshield-1.2.2 → safeshield-1.4.2}/LICENSE +0 -0
  34. {safeshield-1.2.2 → safeshield-1.4.2}/safeshield.egg-info/dependency_links.txt +0 -0
  35. {safeshield-1.2.2 → safeshield-1.4.2}/safeshield.egg-info/top_level.txt +0 -0
  36. {safeshield-1.2.2 → safeshield-1.4.2}/setup.cfg +0 -0
  37. {safeshield-1.2.2 → safeshield-1.4.2}/validator/__init__.py +0 -0
  38. {safeshield-1.2.2 → safeshield-1.4.2}/validator/core/__init__.py +0 -0
  39. {safeshield-1.2.2 → safeshield-1.4.2}/validator/database/__init__.py +0 -0
  40. {safeshield-1.2.2 → safeshield-1.4.2}/validator/database/detector.py +0 -0
  41. {safeshield-1.2.2 → safeshield-1.4.2}/validator/database/manager.py +0 -0
  42. {safeshield-1.2.2 → safeshield-1.4.2}/validator/exceptions.py +0 -0
  43. {safeshield-1.2.2 → safeshield-1.4.2}/validator/services/__init__.py +0 -0
  44. {safeshield-1.2.2 → safeshield-1.4.2}/validator/utils/__init__.py +0 -0
  45. {safeshield-1.2.2 → safeshield-1.4.2}/validator/utils/string.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: safeshield
3
- Version: 1.2.2
3
+ Version: 1.4.2
4
4
  Summary: Library for Help Validation Control
5
5
  Home-page: https://github.com/WunsunTarniho/py-guard
6
6
  Author: Wunsun Tarniho
@@ -58,3 +58,9 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
58
58
 
59
59
  ## v1.2.2 (2024-07-20)
60
60
  - Fixed: Error when connect use postgres.
61
+
62
+ ## v1.3.2 (2024-07-20)
63
+ - Refinement: Groups Inheritance Rule Class.
64
+
65
+ ## v1.4.2 (2024-07-20)
66
+ - Feature: Rule can accept function as argument, Rule Chaning
@@ -38,4 +38,10 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
38
38
  - Fixed: Bug unique and exist rule.
39
39
 
40
40
  ## v1.2.2 (2024-07-20)
41
- - Fixed: Error when connect use postgres.
41
+ - Fixed: Error when connect use postgres.
42
+
43
+ ## v1.3.2 (2024-07-20)
44
+ - Refinement: Groups Inheritance Rule Class.
45
+
46
+ ## v1.4.2 (2024-07-20)
47
+ - Feature: Rule can accept function as argument, Rule Chaning
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: safeshield
3
- Version: 1.2.2
3
+ Version: 1.4.2
4
4
  Summary: Library for Help Validation Control
5
5
  Home-page: https://github.com/WunsunTarniho/py-guard
6
6
  Author: Wunsun Tarniho
@@ -58,3 +58,9 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
58
58
 
59
59
  ## v1.2.2 (2024-07-20)
60
60
  - Fixed: Error when connect use postgres.
61
+
62
+ ## v1.3.2 (2024-07-20)
63
+ - Refinement: Groups Inheritance Rule Class.
64
+
65
+ ## v1.4.2 (2024-07-20)
66
+ - Feature: Rule can accept function as argument, Rule Chaning
@@ -18,13 +18,13 @@ validator/rules/__init__.py
18
18
  validator/rules/array.py
19
19
  validator/rules/base.py
20
20
  validator/rules/basic.py
21
+ validator/rules/boolean.py
21
22
  validator/rules/comparison.py
22
- validator/rules/conditional.py
23
23
  validator/rules/date.py
24
24
  validator/rules/files.py
25
25
  validator/rules/format.py
26
+ validator/rules/numeric.py
26
27
  validator/rules/string.py
27
- validator/rules/type.py
28
28
  validator/rules/utilities.py
29
29
  validator/services/__init__.py
30
30
  validator/services/rule_conflict.py
@@ -6,3 +6,5 @@ python_dateutil==2.9.0.post0
6
6
  setuptools==63.2.0
7
7
  SQLAlchemy==2.0.41
8
8
  Werkzeug==1.0.1
9
+ dnspython==2.4.2
10
+ idna==3.4
@@ -7,7 +7,7 @@ def get_requirements():
7
7
 
8
8
  setup(
9
9
  name="safeshield", # Nama package di PyPI
10
- version="1.2.2",
10
+ version="1.4.2",
11
11
  packages=find_packages(),
12
12
  install_requires=get_requirements(),
13
13
  author="Wunsun Tarniho",
@@ -2,7 +2,7 @@ from typing import Optional, Dict, Any, List, Tuple, Union
2
2
  from validator.exceptions import ValidationException, RuleNotFoundException
3
3
  from validator.factory import RuleFactory
4
4
  from validator.database import DatabaseManager, DatabaseAutoDetector
5
- from validator.rules import ValidationRule
5
+ from validator.rules import Rule
6
6
  from validator.services.rule_conflict import RuleConflictChecker
7
7
  from validator.services.rule_error_handler import RuleErrorHandler
8
8
  from validator.services.rule_preparer import RulePreparer
@@ -12,7 +12,7 @@ from collections.abc import Mapping
12
12
  class Validator:
13
13
  """Main validation class with proper abstractions"""
14
14
  PRIORITY_RULES = {
15
- 'bail', 'exclude_unless', 'exclude_if', 'exclude_with', 'exclude_without', 'required', 'required_if', 'required_unless',
15
+ 'bail', 'sometimes', 'exclude_unless', 'exclude_if', 'exclude_with', 'exclude_without', 'required', 'required_if', 'required_unless',
16
16
  'required_with', 'required_without'
17
17
  }
18
18
 
@@ -23,7 +23,7 @@ class Validator:
23
23
  str,
24
24
  List[Union[
25
25
  str,
26
- ValidationRule,
26
+ Rule,
27
27
  Tuple[str, Union[
28
28
  str,
29
29
  Union[
@@ -38,7 +38,7 @@ class Validator:
38
38
  ]],
39
39
  Tuple[Union[
40
40
  str,
41
- ValidationRule,
41
+ Rule,
42
42
  Tuple[str, Union[
43
43
  str,
44
44
  Union[
@@ -94,7 +94,7 @@ class Validator:
94
94
  return False
95
95
  return True
96
96
 
97
- def _validate_rules(self, prepared_rules: Dict[str, List[ValidationRule]], priority_only: bool):
97
+ def _validate_rules(self, prepared_rules: Dict[str, List[Rule]], priority_only: bool):
98
98
  validated = []
99
99
  for field_pattern, rules in prepared_rules.items():
100
100
  concrete_paths = self._resolve_wildcard_paths(field_pattern) if '*' in field_pattern else [field_pattern]
@@ -217,22 +217,22 @@ class Validator:
217
217
 
218
218
  return _check(self.data, parts)
219
219
 
220
- def _apply_rule(self, field: str, value: Any, rule: ValidationRule) -> bool:
220
+ def _apply_rule(self, field: str, value: Any, rule: Rule) -> bool:
221
221
  try:
222
222
  rule.set_validator(self)
223
223
 
224
- # Format field name untuk pesan error (otomatis tangkap path aktual)
225
224
  display_field = getattr(self, '_current_actual_path', field)
226
225
 
227
226
  if not rule.validate(field, value, getattr(rule, 'params', [])):
228
227
  msg = rule.message(display_field, getattr(rule, 'params', []))
229
- self.error_handler.add_error(display_field, rule.rule_name, msg, value)
228
+ self.error_handler.add_error(display_field, rule.rule_name, getattr(rule, 'params', []), msg, value)
229
+
230
230
  return False
231
231
  else:
232
232
  return True
233
233
  except Exception as e:
234
234
  display_field = getattr(self, '_current_actual_path', field)
235
- self.error_handler.add_error(display_field, rule.rule_name, str(e), value)
235
+ self.error_handler.add_error(display_field, rule.rule_name, getattr(rule, 'params', []), str(e), value)
236
236
  return False
237
237
 
238
238
  def _get_nested_value(self, path: str) -> Any:
@@ -275,7 +275,7 @@ class Validator:
275
275
 
276
276
  return result
277
277
 
278
- def add_rule(self, field: str, rules: Union[str, List[Union[str, ValidationRule]], ValidationRule]):
278
+ def add_rule(self, field: str, rules: Union[str, List[Union[str, Rule]], Rule]):
279
279
  """Add new rules to a field"""
280
280
  new_rules = self.rule_preparer._convert_to_rules(rules)
281
281
  existing_rules = self.rule_preparer._convert_to_rules(self._raw_rules.get(field, []))
@@ -1,20 +1,20 @@
1
1
  # factory.py
2
2
  from typing import Type, Dict, List
3
3
  from validator.rules import all_rules
4
- from validator.rules.base import ValidationRule
4
+ from validator.rules.base import Rule
5
5
 
6
6
  class RuleFactory:
7
- _rules: Dict[str, Type[ValidationRule]] = all_rules
7
+ _rules: Dict[str, Type[Rule]] = all_rules
8
8
 
9
9
  @classmethod
10
- def create_rule(cls, rule_name: str) -> ValidationRule:
10
+ def create_rule(cls, rule_name: str) -> Rule:
11
11
  try:
12
12
  return cls._rules[rule_name]()
13
13
  except KeyError:
14
14
  raise ValueError(f"Unknown validation rule: {rule_name}")
15
15
 
16
16
  @classmethod
17
- def register_rule(cls, name: str, rule_class: Type[ValidationRule]):
17
+ def register_rule(cls, name: str, rule_class: Type[Rule]):
18
18
  cls._rules[name] = rule_class
19
19
 
20
20
  @classmethod
@@ -0,0 +1,24 @@
1
+ import inspect
2
+ from .base import Rule
3
+ from . import array, basic, comparison, date, files, format, boolean, string, numeric, utilities
4
+
5
+ def _collect_rules():
6
+ modules = [array, basic, comparison, date, files, format, boolean, string, numeric, utilities]
7
+ rules = {}
8
+
9
+ for module in modules:
10
+ for name, obj in inspect.getmembers(module):
11
+ if (inspect.isclass(obj) and
12
+ issubclass(obj, Rule) and
13
+ obj != Rule):
14
+ rules[obj.rule_name] = obj
15
+ return rules
16
+
17
+ all_rules = _collect_rules()
18
+
19
+ for name, cls in all_rules.items():
20
+ globals()[cls.__name__.replace('Rule', '')] = cls # Export class name
21
+ globals()[name] = cls # Export rule name
22
+
23
+
24
+ __all__ = ['Rule'] + [cls.__name__.replace('Rule', '') for cls in all_rules.values()]
@@ -1,9 +1,8 @@
1
- from .base import ValidationRule
1
+ from .base import Rule
2
2
  from typing import Any, Dict, List, Optional, Set, Union, Tuple, Type
3
3
 
4
- class ArrayRule(ValidationRule):
4
+ class ArrayRule(Rule):
5
5
  def validate(self, field: str, value: Any, params: List[str]) -> bool:
6
-
7
6
  self._missing_keys = params
8
7
 
9
8
  if params:
@@ -19,44 +18,38 @@ class ArrayRule(ValidationRule):
19
18
 
20
19
  def message(self, field: str, params: List[str]) -> str:
21
20
  if params:
22
- return f"The {field} must contain the keys: {', '.join(self._missing_keys)}."
23
- return f"The {field} must be an array."
24
-
25
- class ContainsRule(ValidationRule):
21
+ return f"The :attribute must contain the keys: {', '.join(self._missing_keys)}."
22
+ return f"The :attribute must be an array."
23
+
24
+ class DistinctRule(Rule):
26
25
  def validate(self, field: str, value: Any, params: List[str]) -> bool:
27
- if not params:
26
+ if not isinstance(value, (list, tuple, set)):
28
27
  return False
29
28
 
30
- search_value = params[0]
29
+ ignore_case = 'ignore_case' in params
31
30
 
32
- # String contains substring
33
- if isinstance(value, str):
34
- return search_value in value
35
-
36
- # Array contains element
37
- if isinstance(value, (list, tuple, set)):
38
- return search_value in value
31
+ seen = set()
32
+ for item in value:
33
+ compare_val = item
39
34
 
40
- # Dictionary contains key
41
- if isinstance(value, dict):
42
- return search_value in value.keys()
35
+ if ignore_case and isinstance(item, str):
36
+ compare_val = item.lower()
43
37
 
44
- return False
45
-
46
- def message(self, field: str, params: List[str]) -> str:
47
- return f"The :name must contain {params[0]}"
48
-
49
- class DistinctRule(ValidationRule):
50
- def validate(self, field: str, value: Any, params: List[str]) -> bool:
51
- if not isinstance(value, (list, tuple, set)):
52
- return False
38
+ if compare_val in seen:
39
+ return False
40
+ seen.add(compare_val)
53
41
 
54
- return len(value) == len(set(value))
42
+ return True
55
43
 
56
44
  def message(self, field: str, params: List[str]) -> str:
57
- return f"The :name must contain unique values"
45
+ base_msg = f"The :attribute must contain unique values"
46
+
47
+ if 'ignore_case' in params:
48
+ return f"{base_msg} (case insensitive)"
49
+ else:
50
+ return f"{base_msg} (strict comparison)"
58
51
 
59
- class InArrayRule(ValidationRule):
52
+ class InArrayRule(Rule):
60
53
  def validate(self, field: str, value: Any, params: List[str]) -> bool:
61
54
  if not params:
62
55
  return False
@@ -64,9 +57,9 @@ class InArrayRule(ValidationRule):
64
57
  return str(value) in params
65
58
 
66
59
  def message(self, field: str, params: List[str]) -> str:
67
- return f"The :name must be one of: {', '.join(params)}"
60
+ return f"The :attribute must be one of: {', '.join(params)}"
68
61
 
69
- class InArrayKeysRule(ValidationRule):
62
+ class InArrayKeysRule(Rule):
70
63
  def validate(self, field: str, value: Any, params: List[str]) -> bool:
71
64
  if not params or not isinstance(value, dict):
72
65
  return False
@@ -74,4 +67,4 @@ class InArrayKeysRule(ValidationRule):
74
67
  return any(key in value for key in params)
75
68
 
76
69
  def message(self, field: str, params: List[str]) -> str:
77
- return f"The :name must contain at least one of these keys: {', '.join(params)}"
70
+ return f"The :attribute must contain at least one of these keys: {', '.join(params)}"
@@ -1,24 +1,47 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from typing import Any, Dict, List, Optional, Set, Union, Tuple, Type
3
3
  from enum import Enum
4
- # from .string import pascal_to_snake
5
4
  import inspect
6
5
  import re
7
6
 
8
- class ValidationRule(ABC):
7
+ class Rule(ABC):
9
8
  """Abstract base class for all validation rules"""
10
9
 
10
+ _rule_classes: Dict[str, Type['Rule']] = {}
11
+ _rule_cache: Dict[str, 'Rule'] = {}
12
+
11
13
  def __init__(self, *params: str):
12
14
  self._validator: Optional['Validator'] = None
13
15
  self._params: List[str] = list(params)
14
16
 
15
- def __init_subclass__(cls):
16
- cls.rule_name = cls.pascal_to_snake(cls.__name__)
17
+ def __init_subclass__(cls, name=None):
18
+ cls.rule_name = cls.pascal_to_snake(cls.__name__) if not hasattr(cls, '_name') else cls._name
19
+ cls._register_rule_class()
20
+ cls._generate_rule_methods()
17
21
 
22
+ @classmethod
23
+ def _register_rule_class(cls):
24
+ cls._rule_classes[cls.rule_name] = cls
25
+
18
26
  @property
19
- def params(self) -> List[str]:
20
- return self._params
27
+ def params(self):
28
+ params = []
29
+ for rule_set in self._params:
30
+ if isinstance(rule_set, (list, tuple)):
31
+ params.append(tuple(rule_set))
32
+ else:
33
+ params.append(rule_set)
34
+ return tuple(params)
21
35
 
36
+ @classmethod
37
+ def _generate_rule_methods(cls):
38
+ for rule_name, rule_class in cls._rule_classes.items():
39
+ @classmethod
40
+ def method(cls, *params, rule_class=rule_class):
41
+ return rule_class(*params)
42
+
43
+ setattr(cls.__class__, rule_name, method)
44
+
22
45
  @params.setter
23
46
  def params(self, value: List[str]) -> None:
24
47
  self._params = value
@@ -64,9 +87,10 @@ class ValidationRule(ABC):
64
87
  """Return the name of the rule for error messages."""
65
88
  pass
66
89
 
67
- def _parse_option_values(self, field: str, params: List[str]) -> List[Any]:
90
+ def _parse_option_values(self, field: str, params: List[str], raise_for_error=True) -> List[Any]:
68
91
  """Parse parameters into allowed values, supporting both Enum class and literal values"""
69
- if not params:
92
+
93
+ if not params and raise_for_error:
70
94
  raise ValueError(
71
95
  f"{self.rule_name} rule requires parameters. "
72
96
  f"Use '({self.rule_name}, EnumClass)' or '{self.rule_name}:val1,val2'"
@@ -0,0 +1,136 @@
1
+ from .base import Rule
2
+ from typing import Any, Dict, List, Optional, Set, Union, Tuple, Type
3
+
4
+ # =============================================
5
+ # BASIC VALIDATION RULES
6
+ # =============================================
7
+
8
+ class AnyOfRule(Rule):
9
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
10
+ self._last_failed_rules = [] # Reset failed rules
11
+
12
+ for i, rule_set in enumerate(params):
13
+ if isinstance(rule_set, str):
14
+ rule_set = [rule_set]
15
+
16
+ from validator import Validator
17
+
18
+ temp_validator = Validator(
19
+ {field: value},
20
+ {field: rule_set},
21
+ db_config=getattr(self._validator, 'db_config', None)
22
+ )
23
+
24
+ if temp_validator.validate():
25
+ return True
26
+ else:
27
+ self._last_failed_rules.append({
28
+ 'rules': rule_set,
29
+ 'errors': temp_validator.errors.get(field, [])
30
+ })
31
+
32
+ return False
33
+
34
+ def message(self, field: str, params: List[str]) -> str:
35
+ if not self._last_failed_rules:
36
+ return f"The :attribute field is invalid."
37
+
38
+ error_messages = []
39
+
40
+ for i, failed in enumerate(self._last_failed_rules, 1):
41
+ rules_str = "|".join(
42
+ r if isinstance(r, str) else getattr(r, 'rule_name', str(r))
43
+ for r in failed['rules']
44
+ )
45
+
46
+ sub_errors = []
47
+ for j, err_msg in enumerate(failed['errors'], 1):
48
+ sub_errors.append(f" {j}. {err_msg}")
49
+
50
+ error_messages.append(
51
+ f"Option {i} (Rules: {rules_str}):\n" +
52
+ "\n".join(sub_errors)
53
+ )
54
+
55
+ return (
56
+ f"The :attribute must satisfy at least one of these conditions:\n" +
57
+ "\n".join(error_messages)
58
+ )
59
+
60
+ class BailRule(Rule):
61
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
62
+ self.validator._stop_on_first_failure = True
63
+ return True
64
+
65
+ def message(self, field: str, params: List[str]) -> str:
66
+ return ""
67
+
68
+ class RequiredRule(Rule):
69
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
70
+ return not self.is_empty(value)
71
+
72
+ def message(self, field: str, params: List[str]) -> str:
73
+ return f"The :attribute field is required."
74
+
75
+ class ProhibitedRule(Rule):
76
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
77
+ return self.is_empty(value)
78
+
79
+ def message(self, field: str, params: List[str]) -> str:
80
+ return "The :attribute field is must be empty."
81
+
82
+ class NullableRule(Rule):
83
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
84
+ return True
85
+
86
+ def message(self, field: str, params: List[str]) -> str:
87
+ return f"The :attribute may be null."
88
+
89
+ class FilledRule(Rule):
90
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
91
+ return value not in ('', None)
92
+
93
+ def message(self, field: str, params: List[str]) -> str:
94
+ return f"The :attribute field must have a value."
95
+
96
+ class PresentRule(Rule):
97
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
98
+ return field in self.validator.data
99
+
100
+ def message(self, field: str, params: List[str]) -> str:
101
+ return f"The :attribute field must be present."
102
+
103
+ class MissingRule(Rule):
104
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
105
+ return value is None
106
+
107
+ def message(self, field: str, params: List[str]) -> str:
108
+ return f"The :attribute field must be missing."
109
+
110
+ class ProhibitsRule(Rule):
111
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
112
+ if not params or value is None:
113
+ return True
114
+ return all(self.get_field_value(param, param) in (None, 'None') for param in params)
115
+
116
+ def message(self, field: str, params: List[str]) -> str:
117
+ return f"When :attribute is present, {', '.join(params)} must be absent."
118
+
119
+ class SometimesRule(Rule):
120
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
121
+ if value is None:
122
+ self.validator._is_exclude = True
123
+
124
+ return True
125
+
126
+ def message(self, field: str, params: List[str]) -> str:
127
+ return ""
128
+
129
+ class ExcludeRule(Rule):
130
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
131
+ self.validator._is_exclude = True
132
+
133
+ return True
134
+
135
+ def message(self, field: str, params: List[str]) -> str:
136
+ return ""
@@ -0,0 +1,157 @@
1
+ from .base import Rule
2
+ from typing import Any, Dict, List, Optional, Set, Union, Tuple, Type
3
+
4
+ class BooleanRule(Rule):
5
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
6
+ if isinstance(value, bool):
7
+ return True
8
+ if isinstance(value, str):
9
+ return value.lower() in ['true', 'false', '1', '0', 'yes', 'no', 'on', 'off']
10
+ if isinstance(value, int):
11
+ return value in [0, 1]
12
+ return False
13
+
14
+ def message(self, field: str, params: List[str]) -> str:
15
+ return f"The :attribute field must be true or false."
16
+
17
+ class AcceptedRule(Rule):
18
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
19
+ value = self.get_field_value(value, value)
20
+
21
+ if isinstance(value, str):
22
+ return value.lower() in ['yes', 'on', '1', 1, True, 'true', 'True']
23
+ if isinstance(value, int):
24
+ return value == 1
25
+ if isinstance(value, bool):
26
+ return value
27
+ return False
28
+
29
+ def message(self, field: str, params: List[str]) -> str:
30
+ return f"The :attribute must be accepted."
31
+
32
+ class DeclinedRule(Rule):
33
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
34
+ value = self.get_field_value(value, value)
35
+
36
+ if isinstance(value, str):
37
+ return value.lower() in ['no', 'off', '0', 0, False, 'false', 'False']
38
+ if isinstance(value, int):
39
+ return value == 0
40
+ if isinstance(value, bool):
41
+ return not value
42
+ return False
43
+
44
+ def message(self, field: str, params: List[str]) -> str:
45
+ return f"The :attribute must be declined."
46
+
47
+ class AcceptedIfRule(AcceptedRule):
48
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
49
+ condition_met = False
50
+
51
+ if len(params) == 1 and callable(params[0]):
52
+ condition_met = params[0](self.validator.data)
53
+ elif len(params) == 1 and isinstance(params[0], bool):
54
+ condition_met = params[0]
55
+ else:
56
+ conditions = list(zip(params[::2], params[1::2]))
57
+
58
+ for other_field, expected_value in conditions:
59
+ if not other_field or expected_value is None:
60
+ continue
61
+
62
+ actual_value = self.get_field_value(other_field, '')
63
+
64
+ if actual_value == expected_value:
65
+ condition_met = True
66
+ break
67
+
68
+ if condition_met:
69
+ return super().validate(field, value, params)
70
+
71
+ return True
72
+
73
+ class AcceptedUnlessRule(AcceptedRule):
74
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
75
+ if len(params) < 2:
76
+ return False
77
+
78
+ other_field, other_value = params[0], params[1]
79
+ actual_value = self.get_field_value(other_field, '')
80
+
81
+ if actual_value == other_value:
82
+ return True
83
+
84
+ return super().validate(field, value, params)
85
+
86
+ class AcceptedAllIfRule(AcceptedRule):
87
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
88
+ if len(params) < 2:
89
+ return False
90
+
91
+ conditions = [(f.strip(), v.strip()) for f, v in zip(params[::2], params[1::2])]
92
+
93
+ all_conditions_met = all(
94
+ self.get_field_value(f) == v
95
+ for f, v in conditions
96
+ )
97
+
98
+ if not all_conditions_met:
99
+ return True
100
+
101
+ return super().validate(field, value, params)
102
+
103
+ class DeclinedIfRule(DeclinedRule):
104
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
105
+ condition_met = False
106
+
107
+ if len(params) == 1 and callable(params[0]):
108
+ condition_met = params[0](self.validator.data)
109
+ elif len(params) == 1 and isinstance(params[0], bool):
110
+ condition_met = params[0]
111
+ else:
112
+ conditions = list(zip(params[::2], params[1::2]))
113
+
114
+ for other_field, expected_value in conditions:
115
+ if not other_field or expected_value is None:
116
+ continue
117
+
118
+ actual_value = self.get_field_value(other_field, '')
119
+
120
+ if actual_value == expected_value:
121
+ condition_met = True
122
+ break
123
+
124
+ if condition_met:
125
+ return super().validate(field, value, params)
126
+
127
+ return True
128
+
129
+ class DeclinedUnlessRule(DeclinedRule):
130
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
131
+ if len(params) < 2:
132
+ return False
133
+
134
+ other_field, other_value = params[0], params[1]
135
+ actual_value = self.get_field_value(other_field, '')
136
+
137
+ if actual_value == other_value:
138
+ return True
139
+
140
+ return super().validate(field, value, params)
141
+
142
+ class DeclinedAllIfRule(DeclinedRule):
143
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
144
+ if len(params) < 2:
145
+ return False
146
+
147
+ conditions = [(f.strip(), v.strip()) for f, v in zip(params[::2], params[1::2])]
148
+
149
+ all_conditions_met = all(
150
+ self.get_field_value(f) == v
151
+ for f, v in conditions
152
+ )
153
+
154
+ if not all_conditions_met:
155
+ return True
156
+
157
+ return super().validate(field, value, params)