safeshield 1.2.2__py3-none-any.whl → 1.4.2__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.
- {safeshield-1.2.2.dist-info → safeshield-1.4.2.dist-info}/METADATA +9 -1
- safeshield-1.4.2.dist-info/RECORD +31 -0
- validator/core/validator.py +10 -10
- validator/factory.py +4 -4
- validator/rules/__init__.py +6 -8
- validator/rules/array.py +27 -34
- validator/rules/base.py +32 -8
- validator/rules/basic.py +105 -10
- validator/rules/boolean.py +157 -0
- validator/rules/comparison.py +130 -40
- validator/rules/date.py +36 -20
- validator/rules/files.py +179 -67
- validator/rules/format.py +122 -31
- validator/rules/numeric.py +188 -0
- validator/rules/string.py +71 -19
- validator/rules/utilities.py +233 -133
- validator/services/rule_conflict.py +2 -2
- validator/services/rule_error_handler.py +221 -24
- validator/services/rule_preparer.py +12 -25
- safeshield-1.2.2.dist-info/RECORD +0 -31
- validator/rules/conditional.py +0 -332
- validator/rules/type.py +0 -42
- {safeshield-1.2.2.dist-info → safeshield-1.4.2.dist-info}/LICENSE +0 -0
- {safeshield-1.2.2.dist-info → safeshield-1.4.2.dist-info}/WHEEL +0 -0
- {safeshield-1.2.2.dist-info → safeshield-1.4.2.dist-info}/top_level.txt +0 -0
validator/rules/files.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
from .base import
|
|
1
|
+
from .base import Rule
|
|
2
2
|
from typing import Any, Dict, List, Optional, Set, Union, Tuple, Type
|
|
3
|
+
import mimetypes
|
|
3
4
|
|
|
4
|
-
class FileRule(
|
|
5
|
+
class FileRule(Rule):
|
|
5
6
|
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
6
7
|
# 1. Cek file object framework (Flask/Werkzeug/FastAPI)
|
|
7
8
|
if (hasattr(value, 'filename') and
|
|
@@ -28,80 +29,173 @@ class FileRule(ValidationRule):
|
|
|
28
29
|
return False
|
|
29
30
|
|
|
30
31
|
def message(self, field: str, params: List[str]) -> str:
|
|
31
|
-
return f"The
|
|
32
|
+
return f"The :attribute must be a valid file"
|
|
32
33
|
|
|
33
|
-
class DimensionsRule(
|
|
34
|
+
class DimensionsRule(Rule):
|
|
35
|
+
def __init__(self, *params):
|
|
36
|
+
super().__init__(*params)
|
|
37
|
+
|
|
38
|
+
self._constraints = {
|
|
39
|
+
'min_width': 0,
|
|
40
|
+
'min_height': 0,
|
|
41
|
+
'max_width': float('inf'),
|
|
42
|
+
'max_height': float('inf'),
|
|
43
|
+
'width': None,
|
|
44
|
+
'height': None,
|
|
45
|
+
'ratio': None
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
def maxWidth(self, value: int) -> 'DimensionsRule':
|
|
49
|
+
self._constraints['width'] = None
|
|
50
|
+
self._constraints['max_width'] = value
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
def maxHeight(self, value: int) -> 'DimensionsRule':
|
|
54
|
+
self._constraints['height'] = None
|
|
55
|
+
self._constraints['max_height'] = value
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
def ratio(self, value: float) -> 'DimensionsRule':
|
|
59
|
+
self._constraints['ratio'] = value
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def minWidth(self, value: int) -> 'DimensionsRule':
|
|
63
|
+
self._constraints['width'] = None
|
|
64
|
+
self._constraints['min_width'] = value
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
def minHeight(self, value: int) -> 'DimensionsRule':
|
|
68
|
+
self._constraints['height'] = None
|
|
69
|
+
self._constraints['min_height'] = value
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
def _validate_constraint_logic(self):
|
|
73
|
+
if self._constraints['width'] is not None:
|
|
74
|
+
if (self._constraints['min_width'] > 0 or
|
|
75
|
+
self._constraints['max_width'] < float('inf')):
|
|
76
|
+
raise ValueError("Cannot specify both exact width and min/max width")
|
|
77
|
+
|
|
78
|
+
if self._constraints['max_width'] < self._constraints['min_width']:
|
|
79
|
+
raise ValueError(f"max_width ({self._constraints['max_width']}) "
|
|
80
|
+
f"cannot be less than min_width ({self._constraints['min_width']})")
|
|
81
|
+
|
|
82
|
+
if self._constraints['height'] is not None:
|
|
83
|
+
if (self._constraints['min_height'] > 0 or
|
|
84
|
+
self._constraints['max_height'] < float('inf')):
|
|
85
|
+
raise ValueError("Cannot specify both exact height and min/max height")
|
|
86
|
+
|
|
87
|
+
if self._constraints['max_height'] < self._constraints['min_height']:
|
|
88
|
+
raise ValueError(f"max_height ({self._constraints['max_height']}) "
|
|
89
|
+
f"cannot be less than min_height ({self._constraints['min_height']})")
|
|
90
|
+
|
|
34
91
|
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
92
|
+
self._validate_constraint_logic()
|
|
35
93
|
try:
|
|
36
|
-
|
|
37
|
-
import io
|
|
38
|
-
|
|
39
|
-
# 1. Parse dimension rules
|
|
40
|
-
min_width = min_height = 0
|
|
41
|
-
max_width = max_height = float('inf')
|
|
42
|
-
|
|
94
|
+
# 2. Parse parameters
|
|
43
95
|
for param in params:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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))
|
|
96
|
+
# Format Laravel: min_width=100
|
|
97
|
+
if '=' in param:
|
|
98
|
+
key, val = param.split('=', 1)
|
|
99
|
+
if key in self._constraints:
|
|
100
|
+
try:
|
|
101
|
+
self._constraints[key] = float(val)
|
|
102
|
+
except ValueError:
|
|
103
|
+
continue
|
|
68
104
|
|
|
69
|
-
#
|
|
70
|
-
elif
|
|
71
|
-
|
|
105
|
+
# Format legacy: 100x200
|
|
106
|
+
elif 'x' in param:
|
|
107
|
+
try:
|
|
108
|
+
width, height = map(int, param.split('x'))
|
|
109
|
+
self._constraints['width'] = width
|
|
110
|
+
self._constraints['height'] = height
|
|
111
|
+
except ValueError:
|
|
112
|
+
continue
|
|
72
113
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
min_height <= height <= max_height)
|
|
114
|
+
# Format ratio: 3/2
|
|
115
|
+
elif '/' in param:
|
|
116
|
+
try:
|
|
117
|
+
self._constraints['ratio'] = param
|
|
118
|
+
except ValueError:
|
|
119
|
+
continue
|
|
80
120
|
|
|
81
|
-
|
|
82
|
-
|
|
121
|
+
# 3. Load image
|
|
122
|
+
img = self._load_image(value)
|
|
123
|
+
if not img:
|
|
83
124
|
return False
|
|
84
125
|
|
|
126
|
+
width, height = img.size
|
|
127
|
+
actual_ratio = round(width / height, 2)
|
|
128
|
+
|
|
129
|
+
# 4. Validate _constraints
|
|
130
|
+
checks = [
|
|
131
|
+
(self._constraints['width'] is None or width == self._constraints['width']),
|
|
132
|
+
(self._constraints['height'] is None or height == self._constraints['height']),
|
|
133
|
+
width >= self._constraints['min_width'],
|
|
134
|
+
height >= self._constraints['min_height'],
|
|
135
|
+
width <= self._constraints['max_width'],
|
|
136
|
+
height <= self._constraints['max_height'],
|
|
137
|
+
self._check_ratio(self._constraints['ratio'], actual_ratio)
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
return all(checks)
|
|
141
|
+
|
|
85
142
|
except Exception as e:
|
|
86
|
-
print(f"
|
|
143
|
+
print(f"Dimension validation error: {str(e)}")
|
|
87
144
|
return False
|
|
88
|
-
|
|
145
|
+
|
|
146
|
+
def _load_image(self, value):
|
|
147
|
+
"""Helper to load image from different sources"""
|
|
148
|
+
try:
|
|
149
|
+
from PIL import Image
|
|
150
|
+
import io
|
|
151
|
+
|
|
152
|
+
if hasattr(value, 'read'): # File-like object
|
|
153
|
+
value.seek(0)
|
|
154
|
+
img = Image.open(value)
|
|
155
|
+
value.seek(0)
|
|
156
|
+
return img
|
|
157
|
+
elif isinstance(value, bytes): # Bytes
|
|
158
|
+
return Image.open(io.BytesIO(value))
|
|
159
|
+
elif isinstance(value, str): # File path
|
|
160
|
+
if value.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')):
|
|
161
|
+
return Image.open(value)
|
|
162
|
+
return None
|
|
163
|
+
except:
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
def _check_ratio(self, ratio_constraint, actual_ratio):
|
|
167
|
+
"""Validate aspect ratio"""
|
|
168
|
+
if not ratio_constraint:
|
|
169
|
+
return True
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
numerator, denominator = map(float, ratio_constraint.split('/'))
|
|
173
|
+
expected_ratio = round(numerator / denominator, 2)
|
|
174
|
+
return actual_ratio == expected_ratio
|
|
175
|
+
except:
|
|
176
|
+
return False
|
|
177
|
+
|
|
89
178
|
def message(self, field: str, params: List[str]) -> str:
|
|
90
|
-
|
|
91
|
-
max_rules = [p for p in params if p.startswith('max:')]
|
|
92
|
-
exact_rules = [p for p in params if ':' not in p]
|
|
179
|
+
constraints = []
|
|
93
180
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
181
|
+
if self._constraints['width'] is not None:
|
|
182
|
+
constraints.append(f"width={self._constraints['width']}")
|
|
183
|
+
if self._constraints['height'] is not None:
|
|
184
|
+
constraints.append(f"height={self._constraints['height']}")
|
|
185
|
+
if self._constraints['min_width'] > 0:
|
|
186
|
+
constraints.append(f"min_width={self._constraints['min_width']}")
|
|
187
|
+
if self._constraints['max_width'] < float('inf'):
|
|
188
|
+
constraints.append(f"max_width={self._constraints['max_width']}")
|
|
189
|
+
if self._constraints['min_height'] > 0:
|
|
190
|
+
constraints.append(f"min_height={self._constraints['min_height']}")
|
|
191
|
+
if self._constraints['max_height'] < float('inf'):
|
|
192
|
+
constraints.append(f"max_height={self._constraints['max_height']}")
|
|
193
|
+
if self._constraints['ratio'] is not None:
|
|
194
|
+
constraints.append(f"ratio={self._constraints['ratio']:.2f}")
|
|
195
|
+
|
|
196
|
+
return f"The :attribute image must satisfy: {', '.join(constraints)}"
|
|
103
197
|
|
|
104
|
-
class ExtensionsRule(
|
|
198
|
+
class ExtensionsRule(Rule):
|
|
105
199
|
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
106
200
|
if not params:
|
|
107
201
|
return False
|
|
@@ -118,9 +212,9 @@ class ExtensionsRule(ValidationRule):
|
|
|
118
212
|
return ext in [e.lower().strip() for e in params]
|
|
119
213
|
|
|
120
214
|
def message(self, field: str, params: List[str]) -> str:
|
|
121
|
-
return f"File
|
|
215
|
+
return f"File :attribute must have one of these extensions: {', '.join(params)}"
|
|
122
216
|
|
|
123
|
-
class ImageRule(
|
|
217
|
+
class ImageRule(Rule):
|
|
124
218
|
VALID_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'}
|
|
125
219
|
VALID_MIME_TYPES = {
|
|
126
220
|
'image/jpeg', 'image/png',
|
|
@@ -146,9 +240,9 @@ class ImageRule(ValidationRule):
|
|
|
146
240
|
return True
|
|
147
241
|
|
|
148
242
|
def message(self, field: str, params: List[str]) -> str:
|
|
149
|
-
return f"The
|
|
243
|
+
return f"The :attribute must be a valid image file (JPEG, PNG, GIF, BMP, or WebP)"
|
|
150
244
|
|
|
151
|
-
class MimeTypesRule(
|
|
245
|
+
class MimeTypesRule(Rule):
|
|
152
246
|
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
153
247
|
if not params:
|
|
154
248
|
return False
|
|
@@ -164,4 +258,22 @@ class MimeTypesRule(ValidationRule):
|
|
|
164
258
|
return False
|
|
165
259
|
|
|
166
260
|
def message(self, field: str, params: List[str]) -> str:
|
|
167
|
-
return f"File
|
|
261
|
+
return f"File :attribute must be one of these types: {', '.join(params)}"
|
|
262
|
+
|
|
263
|
+
class MimeTypeByExtensionRule(Rule):
|
|
264
|
+
_name = 'mimes'
|
|
265
|
+
|
|
266
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
267
|
+
if value is None or not hasattr(value, 'filename'):
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
mimetypes.init()
|
|
271
|
+
|
|
272
|
+
extension = value.filename.split('.')[-1].lower()
|
|
273
|
+
|
|
274
|
+
mime_type = mimetypes.guess_type(f"file.{extension}")[0]
|
|
275
|
+
|
|
276
|
+
return mime_type in params if mime_type else False
|
|
277
|
+
|
|
278
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
279
|
+
return f"The :attribute must be one of these types: {', '.join(params)}"
|
validator/rules/format.py
CHANGED
|
@@ -1,25 +1,129 @@
|
|
|
1
|
-
from .base import
|
|
1
|
+
from .base import Rule
|
|
2
2
|
from typing import Any, Dict, List, Optional, Set, Union, Tuple, Type
|
|
3
3
|
import re
|
|
4
|
-
import zoneinfo
|
|
5
4
|
import ipaddress
|
|
6
5
|
import json
|
|
7
6
|
import uuid
|
|
7
|
+
import dns.resolver
|
|
8
|
+
import idna
|
|
8
9
|
|
|
9
10
|
# =============================================
|
|
10
11
|
# FORMAT VALIDATION RULES
|
|
11
12
|
# =============================================
|
|
12
13
|
|
|
13
|
-
class EmailRule(
|
|
14
|
+
class EmailRule(Rule):
|
|
14
15
|
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
16
|
+
"""
|
|
17
|
+
Available params:
|
|
18
|
+
- rfc: RFCValidation (default)
|
|
19
|
+
- strict: NoRFCWarningsValidation
|
|
20
|
+
- dns: DNSCheckValidation
|
|
21
|
+
- spoof: SpoofCheckValidation
|
|
22
|
+
- filter: FilterEmailValidation
|
|
23
|
+
- filter_unicode: FilterEmailValidation::unicode()
|
|
24
|
+
"""
|
|
25
|
+
|
|
15
26
|
if not isinstance(value, str):
|
|
16
27
|
return False
|
|
17
|
-
|
|
18
|
-
|
|
28
|
+
|
|
29
|
+
# Default to RFC validation if no params specified
|
|
30
|
+
validation_types = params if params else ['rfc']
|
|
31
|
+
|
|
32
|
+
# Apply all requested validations
|
|
33
|
+
for validation in validation_types:
|
|
34
|
+
if validation == 'rfc' and not self._validate_rfc(value):
|
|
35
|
+
return False
|
|
36
|
+
elif validation == 'strict' and not self._validate_strict(value):
|
|
37
|
+
return False
|
|
38
|
+
elif validation == 'dns' and not self._validate_dns(value):
|
|
39
|
+
return False
|
|
40
|
+
elif validation == 'spoof' and not self._validate_spoof(value):
|
|
41
|
+
return False
|
|
42
|
+
elif validation == 'filter' and not self._validate_filter(value):
|
|
43
|
+
return False
|
|
44
|
+
elif validation == 'filter_unicode' and not self._validate_filter_unicode(value):
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
def _validate_rfc(self, email: str) -> bool:
|
|
50
|
+
"""Strict RFC-compliant email validation (single-line version)"""
|
|
51
|
+
return bool(re.match(
|
|
52
|
+
r"^(?!(\.|\.\.))[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@(?=.{1,255}$)[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z]{2,}$",
|
|
53
|
+
email
|
|
54
|
+
))
|
|
55
|
+
|
|
56
|
+
def _validate_strict(self, email: str) -> bool:
|
|
57
|
+
"""Strict RFC validation (no warnings)"""
|
|
58
|
+
if not self._validate_rfc(email):
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
# No leading/trailing dots
|
|
62
|
+
if email.startswith('.') or email.endswith('.'):
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
# No consecutive dots
|
|
66
|
+
if '..' in email.split('@')[0]:
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
def _validate_dns(self, email: str) -> bool:
|
|
72
|
+
"""DNS MX record validation"""
|
|
73
|
+
try:
|
|
74
|
+
domain = email.split('@')[-1]
|
|
75
|
+
return bool(dns.resolver.resolve(domain, 'MX'))
|
|
76
|
+
except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN,
|
|
77
|
+
dns.resolver.NoNameservers, dns.exception.DNSException):
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
def _validate_spoof(self, email: str) -> bool:
|
|
81
|
+
"""Spoof/homograph detection"""
|
|
82
|
+
try:
|
|
83
|
+
domain_part = email.split('@')[-1]
|
|
84
|
+
ascii_domain = idna.encode(domain_part).decode('ascii')
|
|
85
|
+
|
|
86
|
+
# Check for homographs
|
|
87
|
+
if domain_part != ascii_domain:
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
# Check for deceptive characters
|
|
91
|
+
return not any(ord(char) > 127 for char in email)
|
|
92
|
+
except idna.IDNAError:
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
def _validate_filter(self, email: str) -> bool:
|
|
96
|
+
"""PHP filter_var compatible validation"""
|
|
97
|
+
return bool(re.match(
|
|
98
|
+
r"^(?!(\.|\.\.))[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$",
|
|
99
|
+
email
|
|
100
|
+
))
|
|
101
|
+
|
|
102
|
+
def _validate_filter_unicode(self, email: str) -> bool:
|
|
103
|
+
"""PHP filter_var with Unicode support"""
|
|
104
|
+
return bool(re.match(
|
|
105
|
+
r"^(?!(\.|\.\.))[\w.!#$%&'*+/=?^_`{|}~-]+@[\w-]+(?:\.[\w-]+)*$",
|
|
106
|
+
email,
|
|
107
|
+
re.UNICODE
|
|
108
|
+
))
|
|
109
|
+
|
|
19
110
|
def message(self, field: str, params: List[str]) -> str:
|
|
20
|
-
|
|
111
|
+
base_msg = "The :attribute must be a valid email address"
|
|
112
|
+
|
|
113
|
+
if 'strict' in params:
|
|
114
|
+
base_msg += " (strict RFC compliance)"
|
|
115
|
+
if 'dns' in params:
|
|
116
|
+
base_msg += " with valid DNS records"
|
|
117
|
+
if 'spoof' in params:
|
|
118
|
+
base_msg += " without spoofed characters"
|
|
119
|
+
if 'filter' in params:
|
|
120
|
+
base_msg += " (PHP filter compatible)"
|
|
121
|
+
if 'filter_unicode' in params:
|
|
122
|
+
base_msg += " (Unicode allowed)"
|
|
123
|
+
|
|
124
|
+
return f"{base_msg}."
|
|
21
125
|
|
|
22
|
-
class UrlRule(
|
|
126
|
+
class UrlRule(Rule):
|
|
23
127
|
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
24
128
|
if not isinstance(value, str):
|
|
25
129
|
return False
|
|
@@ -32,9 +136,9 @@ class UrlRule(ValidationRule):
|
|
|
32
136
|
))
|
|
33
137
|
|
|
34
138
|
def message(self, field: str, params: List[str]) -> str:
|
|
35
|
-
return f"The :
|
|
139
|
+
return f"The :attribute must be a valid URL."
|
|
36
140
|
|
|
37
|
-
class JsonRule(
|
|
141
|
+
class JsonRule(Rule):
|
|
38
142
|
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
39
143
|
if not isinstance(value, str):
|
|
40
144
|
return False
|
|
@@ -45,9 +149,9 @@ class JsonRule(ValidationRule):
|
|
|
45
149
|
return False
|
|
46
150
|
|
|
47
151
|
def message(self, field: str, params: List[str]) -> str:
|
|
48
|
-
return f"The :
|
|
152
|
+
return f"The :attribute must be a valid JSON string."
|
|
49
153
|
|
|
50
|
-
class UuidRule(
|
|
154
|
+
class UuidRule(Rule):
|
|
51
155
|
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
52
156
|
if not isinstance(value, str):
|
|
53
157
|
return False
|
|
@@ -58,18 +162,18 @@ class UuidRule(ValidationRule):
|
|
|
58
162
|
return False
|
|
59
163
|
|
|
60
164
|
def message(self, field: str, params: List[str]) -> str:
|
|
61
|
-
return f"The :
|
|
165
|
+
return f"The :attribute must be a valid UUID."
|
|
62
166
|
|
|
63
|
-
class UlidRule(
|
|
167
|
+
class UlidRule(Rule):
|
|
64
168
|
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
65
169
|
if not isinstance(value, str):
|
|
66
170
|
return False
|
|
67
171
|
return bool(re.match(r'^[0-9A-HJKMNP-TV-Z]{26}$', value))
|
|
68
172
|
|
|
69
173
|
def message(self, field: str, params: List[str]) -> str:
|
|
70
|
-
return f"The :
|
|
174
|
+
return f"The :attribute must be a valid ULID."
|
|
71
175
|
|
|
72
|
-
class IpRule(
|
|
176
|
+
class IpRule(Rule):
|
|
73
177
|
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
74
178
|
if not isinstance(value, str):
|
|
75
179
|
return False
|
|
@@ -80,26 +184,13 @@ class IpRule(ValidationRule):
|
|
|
80
184
|
return False
|
|
81
185
|
|
|
82
186
|
def message(self, field: str, params: List[str]) -> str:
|
|
83
|
-
return f"The :
|
|
84
|
-
|
|
85
|
-
class TimezoneRule(ValidationRule):
|
|
86
|
-
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
87
|
-
if not isinstance(value, str):
|
|
88
|
-
return False
|
|
89
|
-
try:
|
|
90
|
-
zoneinfo.ZoneInfo(value)
|
|
91
|
-
return True
|
|
92
|
-
except zoneinfo.ZoneInfoNotFoundError:
|
|
93
|
-
return False
|
|
94
|
-
|
|
95
|
-
def message(self, field: str, params: List[str]) -> str:
|
|
96
|
-
return f"The :name must be a valid timezone."
|
|
187
|
+
return f"The :attribute must be a valid IP address."
|
|
97
188
|
|
|
98
|
-
class HexRule(
|
|
189
|
+
class HexRule(Rule):
|
|
99
190
|
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
100
191
|
if not isinstance(value, str):
|
|
101
192
|
return False
|
|
102
193
|
return bool(re.match(r'^#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', value))
|
|
103
194
|
|
|
104
195
|
def message(self, field: str, params: List[str]) -> str:
|
|
105
|
-
return f"The :
|
|
196
|
+
return f"The :attribute must be a valid hexadecimal color code."
|