safeshield 1.0.0__py3-none-any.whl
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.
- safeshield-1.0.0.dist-info/LICENSE +21 -0
- safeshield-1.0.0.dist-info/METADATA +32 -0
- safeshield-1.0.0.dist-info/RECORD +29 -0
- safeshield-1.0.0.dist-info/WHEEL +5 -0
- safeshield-1.0.0.dist-info/top_level.txt +1 -0
- validator/__init__.py +7 -0
- validator/core/__init__.py +3 -0
- validator/core/validator.py +299 -0
- validator/database/__init__.py +5 -0
- validator/database/detector.py +250 -0
- validator/database/manager.py +162 -0
- validator/exceptions.py +10 -0
- validator/factory.py +26 -0
- validator/rules/__init__.py +27 -0
- validator/rules/array.py +77 -0
- validator/rules/base.py +84 -0
- validator/rules/basic.py +41 -0
- validator/rules/comparison.py +240 -0
- validator/rules/conditional.py +332 -0
- validator/rules/date.py +154 -0
- validator/rules/files.py +167 -0
- validator/rules/format.py +105 -0
- validator/rules/string.py +74 -0
- validator/rules/type.py +42 -0
- validator/rules/utilities.py +213 -0
- validator/services/__init__.py +5 -0
- validator/services/rule_conflict.py +133 -0
- validator/services/rule_error_handler.py +42 -0
- validator/services/rule_preparer.py +120 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
from .base import ValidationRule
|
|
2
|
+
from typing import Any, Dict, List, Optional, Set, Union, Tuple, Type
|
|
3
|
+
from enum import Enum
|
|
4
|
+
import re
|
|
5
|
+
import inspect
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
|
|
8
|
+
# =============================================
|
|
9
|
+
# CONDITIONAL VALIDATION RULES
|
|
10
|
+
# =============================================
|
|
11
|
+
|
|
12
|
+
class RequiredIfRule(ValidationRule):
|
|
13
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
14
|
+
if len(params) < 2 or len(params) % 2 != 0:
|
|
15
|
+
return True
|
|
16
|
+
|
|
17
|
+
conditions = list(zip(params[::2], params[1::2]))
|
|
18
|
+
|
|
19
|
+
condition_met = False
|
|
20
|
+
for other_field, expected_value in conditions:
|
|
21
|
+
if not other_field or expected_value is None:
|
|
22
|
+
continue
|
|
23
|
+
|
|
24
|
+
actual_value = self.get_field_value(other_field, '')
|
|
25
|
+
if actual_value == expected_value:
|
|
26
|
+
condition_met = True
|
|
27
|
+
break
|
|
28
|
+
|
|
29
|
+
if not condition_met:
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
return not self.is_empty(value)
|
|
33
|
+
|
|
34
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
35
|
+
valid_conditions = []
|
|
36
|
+
if len(params) >= 2 and len(params) % 2 == 0:
|
|
37
|
+
valid_conditions = [
|
|
38
|
+
f"{params[i]} = {params[i+1]}"
|
|
39
|
+
for i in range(0, len(params), 2)
|
|
40
|
+
if params[i] and params[i+1] is not None
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
if not valid_conditions:
|
|
44
|
+
return f"Invalid required_if rule configuration for {field}"
|
|
45
|
+
|
|
46
|
+
return f"The :name field is required when any of these are true: {', '.join(valid_conditions)}"
|
|
47
|
+
|
|
48
|
+
class RequiredAllIfRule(ValidationRule):
|
|
49
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
50
|
+
if len(params) < 2:
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
conditions = [(f.strip(), v.strip()) for f, v in zip(params[::2], params[1::2])]
|
|
54
|
+
|
|
55
|
+
all_conditions_met = all(
|
|
56
|
+
self.get_field_value(f) == v
|
|
57
|
+
for f, v in conditions
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if not all_conditions_met:
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
return not self.is_empty(value)
|
|
64
|
+
|
|
65
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
66
|
+
conditions = " AND ".join(f"{f} = {v}" for f, v in zip(params[::2], params[1::2]))
|
|
67
|
+
return f"The :name field is required when ALL conditions are met: {conditions}"
|
|
68
|
+
|
|
69
|
+
class RequiredUnlessRule(ValidationRule):
|
|
70
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
71
|
+
if len(params) < 2:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
other_field, other_value = params[0], params[1]
|
|
75
|
+
actual_value = self.get_field_value(other_field, '')
|
|
76
|
+
|
|
77
|
+
if actual_value == other_value:
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
return not self.is_empty(value)
|
|
81
|
+
|
|
82
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
83
|
+
return f"The :name field is required unless {params[0]} is {params[1]}."
|
|
84
|
+
|
|
85
|
+
class RequiredWithRule(ValidationRule):
|
|
86
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
87
|
+
if not params:
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
if any(f in self.validator.data for f in params):
|
|
91
|
+
return not self.is_empty(value)
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
95
|
+
return f"The :name field is required when {', '.join(params)} is present."
|
|
96
|
+
|
|
97
|
+
class RequiredWithAllRule(ValidationRule):
|
|
98
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
99
|
+
if not params:
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
if all(f in self.validator.data for f in params):
|
|
103
|
+
return not self.is_empty(value)
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
107
|
+
return f"The :name field is required when all of {', '.join(params)} are present."
|
|
108
|
+
|
|
109
|
+
class RequiredWithoutRule(ValidationRule):
|
|
110
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
111
|
+
if not params:
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
if any(f not in self.validator.data for f in params):
|
|
115
|
+
return not self.is_empty(value)
|
|
116
|
+
return True
|
|
117
|
+
|
|
118
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
119
|
+
return f"The :name field is required when {', '.join(params)} is not present."
|
|
120
|
+
|
|
121
|
+
class RequiredWithoutAllRule(ValidationRule):
|
|
122
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
123
|
+
if not params:
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
if all(f not in self.validator.data for f in params):
|
|
127
|
+
return not self.is_empty(value)
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
131
|
+
return f"The :name field is required when none of {', '.join(params)} are present."
|
|
132
|
+
|
|
133
|
+
class ProhibitedIfRule(ValidationRule):
|
|
134
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
135
|
+
if len(params) < 2:
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
other_field, other_value = params[0], params[1]
|
|
139
|
+
actual_value = self.get_field_value(other_field, '')
|
|
140
|
+
|
|
141
|
+
if actual_value != other_value:
|
|
142
|
+
return True
|
|
143
|
+
|
|
144
|
+
return self.is_empty(value)
|
|
145
|
+
|
|
146
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
147
|
+
return f"The :name field is prohibited when {params[0]} is {params[1]}."
|
|
148
|
+
|
|
149
|
+
class ProhibitedUnlessRule(ValidationRule):
|
|
150
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
151
|
+
if len(params) < 2:
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
other_field, other_value = params[0], params[1]
|
|
155
|
+
actual_value = self.get_field_value(other_field, '')
|
|
156
|
+
|
|
157
|
+
if actual_value == other_value:
|
|
158
|
+
return True
|
|
159
|
+
|
|
160
|
+
return self.is_empty(value)
|
|
161
|
+
|
|
162
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
163
|
+
return f"The :name field is prohibited unless {params[0]} is {params[1]}."
|
|
164
|
+
|
|
165
|
+
class FilledIfRule(ValidationRule):
|
|
166
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
167
|
+
if len(params) < 2:
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
other_field, other_value = params[0], params[1]
|
|
171
|
+
actual_value = self.get_field_value(other_field, '')
|
|
172
|
+
|
|
173
|
+
if actual_value != other_value:
|
|
174
|
+
return True
|
|
175
|
+
|
|
176
|
+
return value not in ('', None)
|
|
177
|
+
|
|
178
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
179
|
+
return f"The :name field must be filled when {params[0]} is {params[1]}."
|
|
180
|
+
|
|
181
|
+
class RegexRule(ValidationRule):
|
|
182
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
183
|
+
if not params or not isinstance(value, str):
|
|
184
|
+
return False
|
|
185
|
+
try:
|
|
186
|
+
return bool(re.fullmatch(params[0], value))
|
|
187
|
+
except re.error:
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
191
|
+
return f"The :name format is invalid."
|
|
192
|
+
|
|
193
|
+
class NotRegexRule(ValidationRule):
|
|
194
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
195
|
+
if not params or not isinstance(value, str):
|
|
196
|
+
return True
|
|
197
|
+
print(not bool(re.search(params[0], value)))
|
|
198
|
+
try:
|
|
199
|
+
return not bool(re.search(params[0], value))
|
|
200
|
+
except re.error:
|
|
201
|
+
return True
|
|
202
|
+
|
|
203
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
204
|
+
return f"The :name format is invalid."
|
|
205
|
+
|
|
206
|
+
class InRule(ValidationRule):
|
|
207
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
208
|
+
allowed_values = self._parse_option_values(field, params)
|
|
209
|
+
return (str(value) in allowed_values or value in allowed_values)
|
|
210
|
+
|
|
211
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
212
|
+
allowed_values = self._parse_option_values(field, params)
|
|
213
|
+
return f"The selected :name must be in : {', '.join(map(str, allowed_values))}"
|
|
214
|
+
|
|
215
|
+
class NotInRule(ValidationRule):
|
|
216
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
217
|
+
not_allowed_values = self._parse_option_values(field, params)
|
|
218
|
+
return str(value) not in not_allowed_values
|
|
219
|
+
|
|
220
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
221
|
+
not_allowed_values = self._parse_option_values(field, params)
|
|
222
|
+
return f"The selected :name must be not in : {', '.join(map(str, not_allowed_values))}"
|
|
223
|
+
|
|
224
|
+
class EnumRule(ValidationRule):
|
|
225
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
226
|
+
allowed_values = self._parse_option_values(field, params)
|
|
227
|
+
return (str(value) in allowed_values or value in allowed_values)
|
|
228
|
+
|
|
229
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
230
|
+
allowed_values = self._parse_option_values(field, params)
|
|
231
|
+
return f"The :name must be one of: {', '.join(map(str, allowed_values))}"
|
|
232
|
+
|
|
233
|
+
class UniqueRule(ValidationRule):
|
|
234
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
235
|
+
if not params or not hasattr(self.validator, 'db_manager') or not self.validator.db_manager:
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
table = params[0]
|
|
239
|
+
column = field if len(params) == 1 else params[1]
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
# Optional: handle ignore case (id)
|
|
243
|
+
ignore_id = None
|
|
244
|
+
if len(params) > 2 and params[2].startswith('ignore:'):
|
|
245
|
+
ignore_field = params[2].split(':')[1]
|
|
246
|
+
ignore_id = self.get_field_value(ignore_field)
|
|
247
|
+
return self.validator.db_manager.is_unique(table, column, value, ignore_id)
|
|
248
|
+
except Exception as e:
|
|
249
|
+
print(f"Database error in UniqueRule: {e}")
|
|
250
|
+
return False
|
|
251
|
+
|
|
252
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
253
|
+
return f"The :name has already been taken."
|
|
254
|
+
|
|
255
|
+
class ExistsRule(ValidationRule):
|
|
256
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
257
|
+
if not params or not hasattr(self.validator, 'db_manager') or not self.validator.db_manager:
|
|
258
|
+
return False
|
|
259
|
+
|
|
260
|
+
table = params[0]
|
|
261
|
+
column = field if len(params) == 1 else params[1]
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
return self.validator.db_manager.exists(table, column, value)
|
|
265
|
+
except Exception as e:
|
|
266
|
+
print(f"Database error in ExistsRule: {e}")
|
|
267
|
+
return False
|
|
268
|
+
|
|
269
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
270
|
+
return f"The selected :name is invalid."
|
|
271
|
+
|
|
272
|
+
class ConfirmedRule(ValidationRule):
|
|
273
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
274
|
+
confirmation_field = f"{field}_confirmation"
|
|
275
|
+
|
|
276
|
+
return value == self.get_field_value(confirmation_field, '')
|
|
277
|
+
|
|
278
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
279
|
+
return f"The :name confirmation does not match."
|
|
280
|
+
|
|
281
|
+
class SameRule(ValidationRule):
|
|
282
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
283
|
+
if not params:
|
|
284
|
+
return False
|
|
285
|
+
return value == self.get_field_value(params[0])
|
|
286
|
+
|
|
287
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
288
|
+
return f"The :name and {params[0]} must match."
|
|
289
|
+
|
|
290
|
+
class DifferentRule(ValidationRule):
|
|
291
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
292
|
+
if not params:
|
|
293
|
+
return False
|
|
294
|
+
return value != self.get_field_value(params[0])
|
|
295
|
+
|
|
296
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
297
|
+
return f"The :name and {params[0]} must be different."
|
|
298
|
+
|
|
299
|
+
class AcceptedRule(ValidationRule):
|
|
300
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
301
|
+
if isinstance(value, str):
|
|
302
|
+
return value.lower() in ['yes', 'on', '1', 'true']
|
|
303
|
+
if isinstance(value, int):
|
|
304
|
+
return value == 1
|
|
305
|
+
if isinstance(value, bool):
|
|
306
|
+
return value
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
310
|
+
return f"The :name must be accepted."
|
|
311
|
+
|
|
312
|
+
class DeclinedRule(ValidationRule):
|
|
313
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
314
|
+
if isinstance(value, str):
|
|
315
|
+
return value.lower() in ['no', 'off', '0', 'false']
|
|
316
|
+
if isinstance(value, int):
|
|
317
|
+
return value == 0
|
|
318
|
+
if isinstance(value, bool):
|
|
319
|
+
return not value
|
|
320
|
+
return False
|
|
321
|
+
|
|
322
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
323
|
+
return f"The :name must be declined."
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class BailRule(ValidationRule):
|
|
327
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
328
|
+
self.validator._stop_on_first_failure = True
|
|
329
|
+
return True
|
|
330
|
+
|
|
331
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
332
|
+
return ""
|
validator/rules/date.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
from .base import ValidationRule
|
|
2
|
+
from typing import Any, Dict, List, Optional, Set, Union, Tuple, Type
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from dateutil.parser import parse
|
|
5
|
+
|
|
6
|
+
class DateRule(ValidationRule):
|
|
7
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
8
|
+
if not isinstance(value, str):
|
|
9
|
+
return False
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
parse(value)
|
|
13
|
+
return True
|
|
14
|
+
except ValueError:
|
|
15
|
+
return False
|
|
16
|
+
|
|
17
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
18
|
+
return f"The :name is not a valid date."
|
|
19
|
+
|
|
20
|
+
class DateEqualsRule(ValidationRule):
|
|
21
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
22
|
+
if not params or not isinstance(value, str):
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
value = self.get_field_value(value, value)
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
date1 = parse(value)
|
|
29
|
+
|
|
30
|
+
params[0] = self.get_field_value(params[0], params[0])
|
|
31
|
+
|
|
32
|
+
date2 = parse(params[0])
|
|
33
|
+
return date1 == date2
|
|
34
|
+
except ValueError as e:
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
38
|
+
return f"The :name must be equal to {params[0]}."
|
|
39
|
+
|
|
40
|
+
class AfterRule(ValidationRule):
|
|
41
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
42
|
+
if not params or len(params) < 1:
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
# Parse input date
|
|
47
|
+
if isinstance(value, str):
|
|
48
|
+
date_value = parse(value)
|
|
49
|
+
elif isinstance(value, datetime):
|
|
50
|
+
date_value = value
|
|
51
|
+
else:
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
# Parse comparison date
|
|
55
|
+
params[0] = self.get_field_value(params[0], params[0])
|
|
56
|
+
compare_date = parse(params[0])
|
|
57
|
+
|
|
58
|
+
return date_value > compare_date
|
|
59
|
+
|
|
60
|
+
except (ValueError, TypeError):
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
64
|
+
return f"The :name must be after {params[0]}"
|
|
65
|
+
|
|
66
|
+
class AfterOrEqualRule(ValidationRule):
|
|
67
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
68
|
+
if not params or len(params) < 1:
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
if isinstance(value, str):
|
|
73
|
+
date_value = parse(value)
|
|
74
|
+
elif isinstance(value, datetime):
|
|
75
|
+
date_value = value
|
|
76
|
+
else:
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
params[0] = self.get_field_value(params[0], params[0])
|
|
80
|
+
compare_date = parse(params[0])
|
|
81
|
+
|
|
82
|
+
return date_value >= compare_date
|
|
83
|
+
|
|
84
|
+
except (ValueError, TypeError):
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
88
|
+
return f"The :name must be after or equal to {params[0]}"
|
|
89
|
+
|
|
90
|
+
class BeforeRule(ValidationRule):
|
|
91
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
92
|
+
if not params or len(params) < 1:
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
value = self.get_field_value(value, value)
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
if isinstance(value, str):
|
|
99
|
+
date_value = parse(value)
|
|
100
|
+
elif isinstance(value, datetime):
|
|
101
|
+
date_value = value
|
|
102
|
+
else:
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
params[0] = self.get_field_value(params[0], params[0])
|
|
106
|
+
compare_date = parse(params[0])
|
|
107
|
+
|
|
108
|
+
return date_value < compare_date
|
|
109
|
+
|
|
110
|
+
except (ValueError, TypeError):
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
114
|
+
return f"The :name must be before {params[0]}"
|
|
115
|
+
|
|
116
|
+
class BeforeOrEqualRule(ValidationRule):
|
|
117
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
118
|
+
if not params or len(params) < 1:
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
value = self.get_field_value(value, value)
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
if isinstance(value, str):
|
|
125
|
+
date_value = parse(value)
|
|
126
|
+
elif isinstance(value, datetime):
|
|
127
|
+
date_value = value
|
|
128
|
+
else:
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
params[0] = self.get_field_value(params[0], params[0])
|
|
132
|
+
compare_date = parse(params[0])
|
|
133
|
+
|
|
134
|
+
return date_value <= compare_date
|
|
135
|
+
|
|
136
|
+
except (ValueError, TypeError):
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
140
|
+
return f"The :name must be before or equal to {params[0]}"
|
|
141
|
+
|
|
142
|
+
class DateFormatRule(ValidationRule):
|
|
143
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
144
|
+
if not params or len(params) < 1 or not isinstance(value, str):
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
datetime.strptime(value, params[0])
|
|
149
|
+
return True
|
|
150
|
+
except ValueError:
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
154
|
+
return f"The :name must match the format {params[0]}"
|
validator/rules/files.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
from .base import ValidationRule
|
|
2
|
+
from typing import Any, Dict, List, Optional, Set, Union, Tuple, Type
|
|
3
|
+
|
|
4
|
+
class FileRule(ValidationRule):
|
|
5
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
6
|
+
# 1. Cek file object framework (Flask/Werkzeug/FastAPI)
|
|
7
|
+
if (hasattr(value, 'filename') and
|
|
8
|
+
hasattr(value, 'stream') and
|
|
9
|
+
hasattr(value, 'content_type')):
|
|
10
|
+
return True
|
|
11
|
+
|
|
12
|
+
# 2. Cek file-like object umum
|
|
13
|
+
if hasattr(value, 'read') and callable(value.read):
|
|
14
|
+
return True
|
|
15
|
+
|
|
16
|
+
# 3. Cek path file yang valid (string)
|
|
17
|
+
if isinstance(value, str):
|
|
18
|
+
return (
|
|
19
|
+
'.' in value and # Harus punya extension
|
|
20
|
+
not value.startswith('data:') and # Bukan data URI
|
|
21
|
+
not value.strip().startswith('<') # Bukan XML/HTML
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# 4. Cek binary data langsung
|
|
25
|
+
if isinstance(value, (bytes, bytearray)):
|
|
26
|
+
return len(value) > 0 # Pastikan tidak kosong
|
|
27
|
+
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
31
|
+
return f"The {field} must be a valid file"
|
|
32
|
+
|
|
33
|
+
class DimensionsRule(ValidationRule):
|
|
34
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
35
|
+
try:
|
|
36
|
+
from PIL import Image
|
|
37
|
+
import io
|
|
38
|
+
|
|
39
|
+
# 1. Parse dimension rules
|
|
40
|
+
min_width = min_height = 0
|
|
41
|
+
max_width = max_height = float('inf')
|
|
42
|
+
|
|
43
|
+
for param in params:
|
|
44
|
+
try:
|
|
45
|
+
if param.startswith('min:'):
|
|
46
|
+
min_width, min_height = map(int, param[4:].split('x'))
|
|
47
|
+
elif param.startswith('max:'):
|
|
48
|
+
max_width, max_height = map(int, param[4:].split('x'))
|
|
49
|
+
else:
|
|
50
|
+
exact_width, exact_height = map(int, param.split('x'))
|
|
51
|
+
min_width = max_width = exact_width
|
|
52
|
+
min_height = max_height = exact_height
|
|
53
|
+
except (ValueError, IndexError):
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
# 2. Load image with proper handling
|
|
57
|
+
img = None
|
|
58
|
+
try:
|
|
59
|
+
# Case 1: File-like object (Flask/Werkzeug/FastAPI)
|
|
60
|
+
if hasattr(value, 'read'):
|
|
61
|
+
value.seek(0) # Important for rewinding
|
|
62
|
+
img = Image.open(value)
|
|
63
|
+
value.seek(0) # Reset after reading
|
|
64
|
+
|
|
65
|
+
# Case 2: Bytes data
|
|
66
|
+
elif isinstance(value, bytes):
|
|
67
|
+
img = Image.open(io.BytesIO(value))
|
|
68
|
+
|
|
69
|
+
# Case 3: File path (string)
|
|
70
|
+
elif isinstance(value, str) and value.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')):
|
|
71
|
+
img = Image.open(value)
|
|
72
|
+
|
|
73
|
+
if img is None:
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
# 3. Validate dimensions
|
|
77
|
+
width, height = img.size
|
|
78
|
+
return (min_width <= width <= max_width and
|
|
79
|
+
min_height <= height <= max_height)
|
|
80
|
+
|
|
81
|
+
except (IOError, OSError, AttributeError) as e:
|
|
82
|
+
print(f"Image loading failed: {str(e)}")
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
except Exception as e:
|
|
86
|
+
print(f"Unexpected error in dimension validation: {str(e)}")
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
90
|
+
min_rules = [p for p in params if p.startswith('min:')]
|
|
91
|
+
max_rules = [p for p in params if p.startswith('max:')]
|
|
92
|
+
exact_rules = [p for p in params if ':' not in p]
|
|
93
|
+
|
|
94
|
+
messages = []
|
|
95
|
+
if exact_rules:
|
|
96
|
+
messages.append(f"exactly {exact_rules[0]}")
|
|
97
|
+
if min_rules:
|
|
98
|
+
messages.append(f"minimum {min_rules[0][4:]}")
|
|
99
|
+
if max_rules:
|
|
100
|
+
messages.append(f"maximum {max_rules[0][4:]}")
|
|
101
|
+
|
|
102
|
+
return f"Image {field} dimensions must be {' and '.join(messages)}"
|
|
103
|
+
|
|
104
|
+
class ExtensionsRule(ValidationRule):
|
|
105
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
106
|
+
if not params:
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
filename = (
|
|
110
|
+
value.filename if hasattr(value, 'filename')
|
|
111
|
+
else str(value)
|
|
112
|
+
).lower()
|
|
113
|
+
|
|
114
|
+
if '.' not in filename:
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
ext = filename.rsplit('.', 1)[1]
|
|
118
|
+
return ext in [e.lower().strip() for e in params]
|
|
119
|
+
|
|
120
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
121
|
+
return f"File {field} must have one of these extensions: {', '.join(params)}"
|
|
122
|
+
|
|
123
|
+
class ImageRule(ValidationRule):
|
|
124
|
+
VALID_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'}
|
|
125
|
+
VALID_MIME_TYPES = {
|
|
126
|
+
'image/jpeg', 'image/png',
|
|
127
|
+
'image/gif', 'image/bmp', 'image/webp'
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
131
|
+
# Check basic file attributes
|
|
132
|
+
if not hasattr(value, 'filename') and not isinstance(value, (str, bytes)):
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
# Check extension if available
|
|
136
|
+
if hasattr(value, 'filename'):
|
|
137
|
+
ext = value.filename.rsplit('.', 1)[-1].lower()
|
|
138
|
+
if ext not in self.VALID_EXTENSIONS:
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
# Check MIME type if available
|
|
142
|
+
if hasattr(value, 'content_type'):
|
|
143
|
+
if value.content_type not in self.VALID_MIME_TYPES:
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
149
|
+
return f"The {field} must be a valid image file (JPEG, PNG, GIF, BMP, or WebP)"
|
|
150
|
+
|
|
151
|
+
class MimeTypesRule(ValidationRule):
|
|
152
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
153
|
+
if not params:
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
# Check from content_type attribute
|
|
157
|
+
if hasattr(value, 'content_type'):
|
|
158
|
+
return value.content_type in params
|
|
159
|
+
|
|
160
|
+
# Check from mimetype attribute
|
|
161
|
+
if hasattr(value, 'mimetype'):
|
|
162
|
+
return value.mimetype in params
|
|
163
|
+
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
167
|
+
return f"File {field} must be one of these types: {', '.join(params)}"
|