safeshield 1.4.3__tar.gz → 1.4.5__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.
- {safeshield-1.4.3 → safeshield-1.4.5}/PKG-INFO +7 -1
- {safeshield-1.4.3 → safeshield-1.4.5}/README.md +7 -1
- {safeshield-1.4.3 → safeshield-1.4.5}/safeshield.egg-info/PKG-INFO +7 -1
- {safeshield-1.4.3 → safeshield-1.4.5}/setup.py +1 -1
- {safeshield-1.4.3 → safeshield-1.4.5}/validator/core/validator.py +6 -5
- {safeshield-1.4.3 → safeshield-1.4.5}/validator/rules/base.py +1 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/validator/rules/basic.py +2 -2
- safeshield-1.4.5/validator/services/rule_conflict.py +217 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/validator/services/rule_error_handler.py +4 -1
- safeshield-1.4.3/validator/services/rule_conflict.py +0 -133
- {safeshield-1.4.3 → safeshield-1.4.5}/LICENSE +0 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/safeshield.egg-info/SOURCES.txt +0 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/safeshield.egg-info/dependency_links.txt +0 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/safeshield.egg-info/requires.txt +0 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/safeshield.egg-info/top_level.txt +0 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/setup.cfg +0 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/validator/__init__.py +0 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/validator/core/__init__.py +0 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/validator/database/__init__.py +0 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/validator/database/detector.py +0 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/validator/database/manager.py +0 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/validator/exceptions.py +0 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/validator/factory.py +0 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/validator/rules/__init__.py +0 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/validator/rules/array.py +0 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/validator/rules/boolean.py +0 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/validator/rules/comparison.py +0 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/validator/rules/date.py +0 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/validator/rules/files.py +0 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/validator/rules/format.py +0 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/validator/rules/numeric.py +0 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/validator/rules/string.py +0 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/validator/rules/utilities.py +0 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/validator/services/__init__.py +0 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/validator/services/rule_preparer.py +0 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/validator/utils/__init__.py +0 -0
- {safeshield-1.4.3 → safeshield-1.4.5}/validator/utils/string.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: safeshield
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.5
|
|
4
4
|
Summary: Library for Help Validation Control
|
|
5
5
|
Home-page: https://github.com/WunsunTarniho/py-guard
|
|
6
6
|
Author: Wunsun Tarniho
|
|
@@ -67,3 +67,9 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
|
67
67
|
|
|
68
68
|
## v1.4.3 (2024-07-20)
|
|
69
69
|
- Fixed: Error when validation failed
|
|
70
|
+
|
|
71
|
+
## v1.4.3 (2024-07-20)
|
|
72
|
+
- Fixed: Sometimes rule failed
|
|
73
|
+
|
|
74
|
+
## v1.4.4 (2024-07-20)
|
|
75
|
+
- Fixed: Sometimes rule failed
|
|
@@ -47,4 +47,10 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
|
47
47
|
- Feature: Rule can accept function as argument, Rule Chaning
|
|
48
48
|
|
|
49
49
|
## v1.4.3 (2024-07-20)
|
|
50
|
-
- Fixed: Error when validation failed
|
|
50
|
+
- Fixed: Error when validation failed
|
|
51
|
+
|
|
52
|
+
## v1.4.3 (2024-07-20)
|
|
53
|
+
- Fixed: Sometimes rule failed
|
|
54
|
+
|
|
55
|
+
## v1.4.4 (2024-07-20)
|
|
56
|
+
- Fixed: Sometimes rule failed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: safeshield
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.5
|
|
4
4
|
Summary: Library for Help Validation Control
|
|
5
5
|
Home-page: https://github.com/WunsunTarniho/py-guard
|
|
6
6
|
Author: Wunsun Tarniho
|
|
@@ -67,3 +67,9 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
|
67
67
|
|
|
68
68
|
## v1.4.3 (2024-07-20)
|
|
69
69
|
- Fixed: Error when validation failed
|
|
70
|
+
|
|
71
|
+
## v1.4.3 (2024-07-20)
|
|
72
|
+
- Fixed: Sometimes rule failed
|
|
73
|
+
|
|
74
|
+
## v1.4.4 (2024-07-20)
|
|
75
|
+
- Fixed: Sometimes rule failed
|
|
@@ -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', 'sometimes', 'exclude_unless', 'exclude_if', 'exclude_with', 'exclude_without', 'required', 'required_if', 'required_unless',
|
|
15
|
+
'bail', 'sometimes', 'exclude', 'exclude_unless', 'exclude_if', 'exclude_with', 'exclude_without', 'required', 'required_if', 'required_unless',
|
|
16
16
|
'required_with', 'required_without'
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -61,7 +61,8 @@ class Validator:
|
|
|
61
61
|
self.data = data or {}
|
|
62
62
|
self._raw_rules = rules or {}
|
|
63
63
|
self._stop_on_first_failure = False
|
|
64
|
-
self.
|
|
64
|
+
self._field_to_exclude = []
|
|
65
|
+
|
|
65
66
|
|
|
66
67
|
# Initialize dependencies
|
|
67
68
|
self.rule_preparer = RulePreparer(RuleFactory())
|
|
@@ -119,7 +120,7 @@ class Validator:
|
|
|
119
120
|
)
|
|
120
121
|
|
|
121
122
|
for rule in rules:
|
|
122
|
-
if priority_only == (rule.rule_name in self.PRIORITY_RULES)
|
|
123
|
+
if priority_only == (rule.rule_name in self.PRIORITY_RULES):
|
|
123
124
|
rule.set_field_exists(field_exists)
|
|
124
125
|
|
|
125
126
|
# Skip validation for empty array with wildcard unless it's a required rule
|
|
@@ -127,10 +128,10 @@ class Validator:
|
|
|
127
128
|
continue
|
|
128
129
|
|
|
129
130
|
for value in values_to_validate:
|
|
130
|
-
|
|
131
|
+
if field_pattern not in self._field_to_exclude:
|
|
132
|
+
validated.append(self._apply_rule(field_pattern, value, rule))
|
|
131
133
|
|
|
132
134
|
delattr(self, '_current_actual_path')
|
|
133
|
-
self._is_exclude = False
|
|
134
135
|
|
|
135
136
|
return all(valid for valid in validated)
|
|
136
137
|
|
|
@@ -119,7 +119,7 @@ class ProhibitsRule(Rule):
|
|
|
119
119
|
class SometimesRule(Rule):
|
|
120
120
|
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
121
121
|
if value is None:
|
|
122
|
-
self.validator.
|
|
122
|
+
self.validator._field_to_exclude.append(field)
|
|
123
123
|
|
|
124
124
|
return True
|
|
125
125
|
|
|
@@ -128,7 +128,7 @@ class SometimesRule(Rule):
|
|
|
128
128
|
|
|
129
129
|
class ExcludeRule(Rule):
|
|
130
130
|
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
131
|
-
self.validator.
|
|
131
|
+
self.validator._field_to_exclude.append(field)
|
|
132
132
|
|
|
133
133
|
return True
|
|
134
134
|
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
from typing import List, Set, Dict, Tuple
|
|
2
|
+
import warnings
|
|
3
|
+
|
|
4
|
+
class RuleConflictChecker:
|
|
5
|
+
"""Class untuk mendeteksi dan menangani konflik antar validation rules."""
|
|
6
|
+
|
|
7
|
+
CRITICAL_CONFLICTS = [
|
|
8
|
+
('required', 'nullable'), # Cannot be both mandatory and optional
|
|
9
|
+
('required', 'sometimes'), # Sometimes overrides required
|
|
10
|
+
('filled', 'prohibited'), # Cannot require and prohibit simultaneously
|
|
11
|
+
('present', 'missing'), # Field can't be both present and missing
|
|
12
|
+
('accepted', 'declined'), # Values must be opposite
|
|
13
|
+
('same', 'different'), # Direct logical opposites
|
|
14
|
+
|
|
15
|
+
('required', 'nullable'), # Cannot be both mandatory and optional
|
|
16
|
+
('required', 'sometimes'), # Sometimes overrides required
|
|
17
|
+
('filled', 'prohibited'), # Cannot require and prohibit simultaneously
|
|
18
|
+
('present', 'missing'), # Field can't be both present and missing
|
|
19
|
+
('accepted', 'declined'), # Values must be opposite
|
|
20
|
+
('same', 'different'), # Direct logical opposites
|
|
21
|
+
|
|
22
|
+
# Numeric vs Other Types
|
|
23
|
+
('numeric', 'array'),
|
|
24
|
+
('numeric', 'boolean'),
|
|
25
|
+
('numeric', 'email'),
|
|
26
|
+
('numeric', 'date'),
|
|
27
|
+
|
|
28
|
+
# String vs Other Types
|
|
29
|
+
('string', 'array'),
|
|
30
|
+
('string', 'file'),
|
|
31
|
+
('string', 'json'),
|
|
32
|
+
|
|
33
|
+
# Special Types
|
|
34
|
+
('boolean', 'integer'),
|
|
35
|
+
('file', 'image'), # All images are files but not vice versa
|
|
36
|
+
('uuid', 'ulid'), # Similar but incompatible formats
|
|
37
|
+
('hex', 'alpha_num'),
|
|
38
|
+
|
|
39
|
+
# Email/URL/IP Conflicts
|
|
40
|
+
('email', 'ip'),
|
|
41
|
+
('email', 'url'),
|
|
42
|
+
('url', 'json'),
|
|
43
|
+
|
|
44
|
+
# Date/Time Conflicts
|
|
45
|
+
('date', 'timezone'),
|
|
46
|
+
('date_format', 'date_equals'),
|
|
47
|
+
('after', 'before'), # Illogical date ranges
|
|
48
|
+
|
|
49
|
+
# Special Formats
|
|
50
|
+
('regex', 'not_regex'), # Direct pattern negation
|
|
51
|
+
('ascii', 'alpha_dash'), # ASCII vs extended charset
|
|
52
|
+
('uppercase', 'lowercase'), # Case transformation conflicts
|
|
53
|
+
|
|
54
|
+
# Direct Value Checks
|
|
55
|
+
('in', 'not_in'),
|
|
56
|
+
('starts_with', 'doesnt_start_with'),
|
|
57
|
+
('ends_with', 'doesnt_end_with'),
|
|
58
|
+
|
|
59
|
+
# Range Conflicts
|
|
60
|
+
('between', 'digits_between'),
|
|
61
|
+
('min', 'gt'),
|
|
62
|
+
('max', 'lt'),
|
|
63
|
+
('size', 'max_digits'),
|
|
64
|
+
|
|
65
|
+
# Numeric Constraints
|
|
66
|
+
('multiple_of', 'digits_between'),
|
|
67
|
+
('integer', 'decimal'),
|
|
68
|
+
|
|
69
|
+
# Required Group
|
|
70
|
+
('required_if', 'exclude_if'),
|
|
71
|
+
('required_unless', 'exclude_unless'),
|
|
72
|
+
|
|
73
|
+
# Presence Group
|
|
74
|
+
('present_if', 'missing_if'),
|
|
75
|
+
('present_unless', 'missing_unless'),
|
|
76
|
+
|
|
77
|
+
# Acceptance Group
|
|
78
|
+
('accepted_if', 'declined_if'),
|
|
79
|
+
('accepted_unless', 'declined_unless'),
|
|
80
|
+
|
|
81
|
+
# Prohibited Group
|
|
82
|
+
('prohibited_if', 'required_if'),
|
|
83
|
+
('prohibited_unless', 'required_unless'),
|
|
84
|
+
|
|
85
|
+
('file', 'dimensions'), # Dimensions only for images
|
|
86
|
+
('image', 'mime_types'),
|
|
87
|
+
('extensions', 'mimes'),
|
|
88
|
+
('file', 'json'), # Can't be both file and JSON
|
|
89
|
+
|
|
90
|
+
('array', 'distinct'), # Distinct requires array
|
|
91
|
+
('in_array', 'contains'), # Similar containment checks
|
|
92
|
+
('array_keys', 'in_array'),
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
WARNING_CONFLICTS = [
|
|
96
|
+
('integer', 'numeric'), # integer implies numeric check
|
|
97
|
+
('digits', 'digits_between'), # digits:5 is same as digits_between:5,5
|
|
98
|
+
('decimal', 'numeric'), # decimal is subset of numeric
|
|
99
|
+
('multiple_of', 'numeric'), # multiple_of requires numeric
|
|
100
|
+
('min', 'gt'), # similar lower-bound checks
|
|
101
|
+
('max', 'lt'), # similar upper-bound checks
|
|
102
|
+
('digits_between', 'between'), # overlapping digit vs value ranges
|
|
103
|
+
|
|
104
|
+
('alpha', 'alpha_num'), # alpha_num includes alpha
|
|
105
|
+
('alpha_dash', 'alpha_num'), # alpha_dash extends alpha_num
|
|
106
|
+
('ascii', 'alpha'), # alpha implies ASCII
|
|
107
|
+
('uppercase', 'lowercase'), # mutually exclusive cases
|
|
108
|
+
('starts_with', 'ends_with'), # potentially redundant patterns
|
|
109
|
+
('regex', 'ascii'), # regex might duplicate ASCII check
|
|
110
|
+
('contains', 'in_array'), # similar containment checks
|
|
111
|
+
|
|
112
|
+
('after', 'after_or_equal'), # _or_equal is more inclusive
|
|
113
|
+
('before', 'before_or_equal'), # _or_equal is more inclusive
|
|
114
|
+
('date_format', 'date'), # date implies format validation
|
|
115
|
+
('timezone', 'date_format'), # timezone implies format
|
|
116
|
+
('date_equals', 'after_or_equal'), # potential overlap
|
|
117
|
+
|
|
118
|
+
('file', 'image'), # image implies file check
|
|
119
|
+
('mime_types', 'mime_type_by_extension'), # similar type checks
|
|
120
|
+
('dimensions', 'image'), # dimensions requires image
|
|
121
|
+
|
|
122
|
+
('array', 'distinct'), # distinct requires array
|
|
123
|
+
('in_array', 'contains'), # similar element checks
|
|
124
|
+
('array_keys', 'in_array'), # similar key existence checks
|
|
125
|
+
|
|
126
|
+
('required_if', 'required_with'), # similar conditional logic
|
|
127
|
+
('prohibited_if', 'prohibited_unless'), # inverse conditions
|
|
128
|
+
('present_with', 'missing_with'), # mutually aware rules
|
|
129
|
+
|
|
130
|
+
('string', 'alpha'), # string conversion implied
|
|
131
|
+
('numeric', 'integer'), # numeric conversion implied
|
|
132
|
+
('boolean', 'accepted'), # boolean conversion implied
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
REQUIRED_GROUPS = {
|
|
136
|
+
'required_if', 'required_unless', 'required_with', 'required_with_all',
|
|
137
|
+
'required_without', 'required_without_all', 'required_if_accepted', 'required_if_declined'
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
PROHIBITED_GROUPS = {
|
|
141
|
+
'prohibited_if', 'prohibited_unless', 'prohibited_if_accepted', 'prohibited_if_declined'
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
EXCLUSION_GROUPS = {
|
|
145
|
+
'exclude_if', 'exclude_unless', 'exclude_with', 'exclude_without'
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
PRESENCE_GROUPS = {
|
|
149
|
+
'present_if', 'present_unless', 'present_with', 'present_with_all',
|
|
150
|
+
'missing_if', 'missing_unless', 'missing_with', 'missing_with_all'
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@classmethod
|
|
154
|
+
def check_conflicts(cls, rules: List['Rule']) -> None:
|
|
155
|
+
rule_names = {r.rule_name for r in rules}
|
|
156
|
+
params_map = {r.rule_name: r.params for r in rules}
|
|
157
|
+
|
|
158
|
+
cls._check_critical_conflicts(rule_names)
|
|
159
|
+
cls._check_warning_conflicts(rule_names)
|
|
160
|
+
cls._check_parameter_conflicts(rule_names, params_map)
|
|
161
|
+
cls._check_special_cases(rule_names)
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def _check_critical_conflicts(cls, rule_names: Set[str]) -> None:
|
|
165
|
+
"""Cek konflik kritis yang akan memunculkan exception."""
|
|
166
|
+
for rule1, rule2 in cls.CRITICAL_CONFLICTS:
|
|
167
|
+
if rule1 in rule_names and rule2 in rule_names:
|
|
168
|
+
raise ValueError(
|
|
169
|
+
f"Critical rule conflict: '{rule1}' cannot be used with '{rule2}'"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def _check_warning_conflicts(cls, rule_names: Set[str]) -> None:
|
|
174
|
+
"""Cek konflik fungsional yang hanya memunculkan warning."""
|
|
175
|
+
for rule1, rule2 in cls.WARNING_CONFLICTS:
|
|
176
|
+
if rule1 in rule_names and rule2 in rule_names:
|
|
177
|
+
warnings.warn(f"Potential overlap: '{rule1}' and '{rule2}' may validate similar things", UserWarning, stacklevel=2)
|
|
178
|
+
|
|
179
|
+
@classmethod
|
|
180
|
+
def _check_parameter_conflicts(cls, rule_names: Set[str], params_map: Dict[str, List[str]]) -> None:
|
|
181
|
+
"""Cek konflik parameter antar rules."""
|
|
182
|
+
# Range conflicts
|
|
183
|
+
if 'min' in rule_names and 'max' in rule_names:
|
|
184
|
+
min_val = float(params_map['min'][0])
|
|
185
|
+
max_val = float(params_map['max'][0])
|
|
186
|
+
if min_val > max_val:
|
|
187
|
+
raise ValueError(f"Invalid range: min ({min_val}) > max ({max_val})")
|
|
188
|
+
|
|
189
|
+
if 'between' in rule_names:
|
|
190
|
+
between_vals = params_map['between']
|
|
191
|
+
|
|
192
|
+
if len(between_vals) != 2:
|
|
193
|
+
raise ValueError("Between rule requires exactly 2 values")
|
|
194
|
+
min_val, max_val = map(float, between_vals)
|
|
195
|
+
if min_val >= max_val:
|
|
196
|
+
raise ValueError(f"Invalid between range: {min_val} >= {max_val}")
|
|
197
|
+
|
|
198
|
+
# Size vs length checks
|
|
199
|
+
if 'size' in rule_names and ('min' in rule_names or 'max' in rule_names):
|
|
200
|
+
warnings.warn("'size' already implies exact dimension, 'min/max' may be redundant", UserWarning)
|
|
201
|
+
|
|
202
|
+
@classmethod
|
|
203
|
+
def _check_special_cases(cls, rule_names: Set[str]) -> None:
|
|
204
|
+
"""Cek special cases dan grup rules."""
|
|
205
|
+
# Required_with/without group conflicts
|
|
206
|
+
if len(cls.REQUIRED_GROUPS & rule_names) > 1:
|
|
207
|
+
warnings.warn(
|
|
208
|
+
"Multiple required_* conditions may cause unexpected behavior",
|
|
209
|
+
UserWarning
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Prohibited_if/unless conflicts
|
|
213
|
+
if len(cls.PROHIBITED_GROUPS & rule_names) > 1:
|
|
214
|
+
warnings.warn(
|
|
215
|
+
"Using both prohibited_if and prohibited_unless may be confusing",
|
|
216
|
+
UserWarning
|
|
217
|
+
)
|
|
@@ -162,12 +162,15 @@ class RuleErrorHandler:
|
|
|
162
162
|
# Rules with field-value pairs (field1,value1,field2,value2,...)
|
|
163
163
|
if rule in {
|
|
164
164
|
'required_if', 'required_unless', 'exclude_if', 'exclude_unless',
|
|
165
|
+
'accepted_if', 'accepted_all_if', 'declined_if', 'declined_all_if',
|
|
166
|
+
'accepted_unless', 'declined_unless',
|
|
165
167
|
'missing_if', 'missing_unless', 'present_if', 'present_unless'
|
|
166
168
|
}:
|
|
167
169
|
return self._current_params[::2] # Take every even index
|
|
168
170
|
|
|
169
|
-
# Rules with
|
|
171
|
+
# Rules with multi field references
|
|
170
172
|
if rule in {
|
|
173
|
+
'required_with', 'required_with_all', 'required_without', 'required_without_all',
|
|
171
174
|
'prohibits', 'exclude_with', 'exclude_without',
|
|
172
175
|
'missing_with', 'missing_with_all',
|
|
173
176
|
'present_with', 'present_with_all'
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
from typing import List, Set, Dict, Tuple
|
|
2
|
-
import warnings
|
|
3
|
-
|
|
4
|
-
class RuleConflictChecker:
|
|
5
|
-
"""Class untuk mendeteksi dan menangani konflik antar validation rules."""
|
|
6
|
-
|
|
7
|
-
# Daftar konflik kritis yang akan memunculkan exception
|
|
8
|
-
CRITICAL_CONFLICTS = [
|
|
9
|
-
# Mutually exclusive presence
|
|
10
|
-
('required', 'nullable'),
|
|
11
|
-
('required', 'sometimes'),
|
|
12
|
-
('filled', 'prohibited_if'),
|
|
13
|
-
|
|
14
|
-
# Type conflicts
|
|
15
|
-
('numeric', 'email'),
|
|
16
|
-
('numeric', 'array'),
|
|
17
|
-
('boolean', 'integer'),
|
|
18
|
-
('boolean', 'numeric'),
|
|
19
|
-
('array', 'string'),
|
|
20
|
-
|
|
21
|
-
# Format conflicts
|
|
22
|
-
('email', 'ip'),
|
|
23
|
-
('uuid', 'ulid'),
|
|
24
|
-
('json', 'timezone'),
|
|
25
|
-
|
|
26
|
-
# Value requirement conflicts
|
|
27
|
-
('accepted', 'declined'),
|
|
28
|
-
('same', 'different')
|
|
29
|
-
]
|
|
30
|
-
|
|
31
|
-
# Daftar konflik fungsional yang hanya memunculkan warning
|
|
32
|
-
WARNING_CONFLICTS = [
|
|
33
|
-
# Overlapping validation
|
|
34
|
-
('between', 'digits_between'),
|
|
35
|
-
('min', 'size'),
|
|
36
|
-
('max', 'size'),
|
|
37
|
-
('confirmed', 'same'),
|
|
38
|
-
('in', 'not_in'),
|
|
39
|
-
|
|
40
|
-
# Redundant type checks
|
|
41
|
-
('integer', 'numeric'),
|
|
42
|
-
('alpha_num', 'alpha_dash'),
|
|
43
|
-
('starts_with', 'ends_with'),
|
|
44
|
-
|
|
45
|
-
# Similar format checks
|
|
46
|
-
('url', 'json'),
|
|
47
|
-
('ulid', 'uuid')
|
|
48
|
-
]
|
|
49
|
-
|
|
50
|
-
# Grup rules yang saling terkait
|
|
51
|
-
REQUIRED_GROUPS = {
|
|
52
|
-
'required_with', 'required_with_all',
|
|
53
|
-
'required_without', 'required_without_all'
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
PROHIBITED_GROUPS = {
|
|
57
|
-
'prohibited_if', 'prohibited_unless'
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
@classmethod
|
|
61
|
-
def check_conflicts(cls, rules: List['Rule']) -> None:
|
|
62
|
-
"""Main method untuk mengecek semua jenis konflik.
|
|
63
|
-
|
|
64
|
-
Args:
|
|
65
|
-
rules: List of Rule objects to check
|
|
66
|
-
|
|
67
|
-
Raises:
|
|
68
|
-
ValueError: Untuk konflik kritis
|
|
69
|
-
UserWarning: Untuk konflik fungsional/potensial
|
|
70
|
-
"""
|
|
71
|
-
rule_names = {r.rule_name for r in rules}
|
|
72
|
-
params_map = {r.rule_name: r.params for r in rules}
|
|
73
|
-
|
|
74
|
-
cls._check_critical_conflicts(rule_names)
|
|
75
|
-
cls._check_warning_conflicts(rule_names)
|
|
76
|
-
cls._check_parameter_conflicts(rule_names, params_map)
|
|
77
|
-
cls._check_special_cases(rule_names)
|
|
78
|
-
|
|
79
|
-
@classmethod
|
|
80
|
-
def _check_critical_conflicts(cls, rule_names: Set[str]) -> None:
|
|
81
|
-
"""Cek konflik kritis yang akan memunculkan exception."""
|
|
82
|
-
for rule1, rule2 in cls.CRITICAL_CONFLICTS:
|
|
83
|
-
if rule1 in rule_names and rule2 in rule_names:
|
|
84
|
-
raise ValueError(
|
|
85
|
-
f"Critical rule conflict: '{rule1}' cannot be used with '{rule2}'"
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
@classmethod
|
|
89
|
-
def _check_warning_conflicts(cls, rule_names: Set[str]) -> None:
|
|
90
|
-
"""Cek konflik fungsional yang hanya memunculkan warning."""
|
|
91
|
-
for rule1, rule2 in cls.WARNING_CONFLICTS:
|
|
92
|
-
if rule1 in rule_names and rule2 in rule_names:
|
|
93
|
-
warnings.warn(f"Potential overlap: '{rule1}' and '{rule2}' may validate similar things", UserWarning, stacklevel=2)
|
|
94
|
-
|
|
95
|
-
@classmethod
|
|
96
|
-
def _check_parameter_conflicts(cls, rule_names: Set[str], params_map: Dict[str, List[str]]) -> None:
|
|
97
|
-
"""Cek konflik parameter antar rules."""
|
|
98
|
-
# Range conflicts
|
|
99
|
-
if 'min' in rule_names and 'max' in rule_names:
|
|
100
|
-
min_val = float(params_map['min'][0])
|
|
101
|
-
max_val = float(params_map['max'][0])
|
|
102
|
-
if min_val > max_val:
|
|
103
|
-
raise ValueError(f"Invalid range: min ({min_val}) > max ({max_val})")
|
|
104
|
-
|
|
105
|
-
if 'between' in rule_names:
|
|
106
|
-
between_vals = params_map['between']
|
|
107
|
-
|
|
108
|
-
if len(between_vals) != 2:
|
|
109
|
-
raise ValueError("Between rule requires exactly 2 values")
|
|
110
|
-
min_val, max_val = map(float, between_vals)
|
|
111
|
-
if min_val >= max_val:
|
|
112
|
-
raise ValueError(f"Invalid between range: {min_val} >= {max_val}")
|
|
113
|
-
|
|
114
|
-
# Size vs length checks
|
|
115
|
-
if 'size' in rule_names and ('min' in rule_names or 'max' in rule_names):
|
|
116
|
-
warnings.warn("'size' already implies exact dimension, 'min/max' may be redundant", UserWarning)
|
|
117
|
-
|
|
118
|
-
@classmethod
|
|
119
|
-
def _check_special_cases(cls, rule_names: Set[str]) -> None:
|
|
120
|
-
"""Cek special cases dan grup rules."""
|
|
121
|
-
# Required_with/without group conflicts
|
|
122
|
-
if len(cls.REQUIRED_GROUPS & rule_names) > 1:
|
|
123
|
-
warnings.warn(
|
|
124
|
-
"Multiple required_* conditions may cause unexpected behavior",
|
|
125
|
-
UserWarning
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
# Prohibited_if/unless conflicts
|
|
129
|
-
if len(cls.PROHIBITED_GROUPS & rule_names) > 1:
|
|
130
|
-
warnings.warn(
|
|
131
|
-
"Using both prohibited_if and prohibited_unless may be confusing",
|
|
132
|
-
UserWarning
|
|
133
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|