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,105 @@
|
|
|
1
|
+
from .base import ValidationRule
|
|
2
|
+
from typing import Any, Dict, List, Optional, Set, Union, Tuple, Type
|
|
3
|
+
import re
|
|
4
|
+
import zoneinfo
|
|
5
|
+
import ipaddress
|
|
6
|
+
import json
|
|
7
|
+
import uuid
|
|
8
|
+
|
|
9
|
+
# =============================================
|
|
10
|
+
# FORMAT VALIDATION RULES
|
|
11
|
+
# =============================================
|
|
12
|
+
|
|
13
|
+
class EmailRule(ValidationRule):
|
|
14
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
15
|
+
if not isinstance(value, str):
|
|
16
|
+
return False
|
|
17
|
+
return bool(re.match(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$", value))
|
|
18
|
+
|
|
19
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
20
|
+
return f"The :name must be a valid email address."
|
|
21
|
+
|
|
22
|
+
class UrlRule(ValidationRule):
|
|
23
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
24
|
+
if not isinstance(value, str):
|
|
25
|
+
return False
|
|
26
|
+
return bool(re.match(
|
|
27
|
+
r'^(https?:\/\/)?' # protocol
|
|
28
|
+
r'([\da-z\.-]+)\.' # domain
|
|
29
|
+
r'([a-z\.]{2,6})' # top level domain
|
|
30
|
+
r'([\/\w \.-]*)*\/?$', # path/query
|
|
31
|
+
value
|
|
32
|
+
))
|
|
33
|
+
|
|
34
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
35
|
+
return f"The :name must be a valid URL."
|
|
36
|
+
|
|
37
|
+
class JsonRule(ValidationRule):
|
|
38
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
39
|
+
if not isinstance(value, str):
|
|
40
|
+
return False
|
|
41
|
+
try:
|
|
42
|
+
json.loads(value)
|
|
43
|
+
return True
|
|
44
|
+
except json.JSONDecodeError:
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
48
|
+
return f"The :name must be a valid JSON string."
|
|
49
|
+
|
|
50
|
+
class UuidRule(ValidationRule):
|
|
51
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
52
|
+
if not isinstance(value, str):
|
|
53
|
+
return False
|
|
54
|
+
try:
|
|
55
|
+
uuid.UUID(value)
|
|
56
|
+
return True
|
|
57
|
+
except ValueError:
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
61
|
+
return f"The :name must be a valid UUID."
|
|
62
|
+
|
|
63
|
+
class UlidRule(ValidationRule):
|
|
64
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
65
|
+
if not isinstance(value, str):
|
|
66
|
+
return False
|
|
67
|
+
return bool(re.match(r'^[0-9A-HJKMNP-TV-Z]{26}$', value))
|
|
68
|
+
|
|
69
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
70
|
+
return f"The :name must be a valid ULID."
|
|
71
|
+
|
|
72
|
+
class IpRule(ValidationRule):
|
|
73
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
74
|
+
if not isinstance(value, str):
|
|
75
|
+
return False
|
|
76
|
+
try:
|
|
77
|
+
ipaddress.ip_address(value)
|
|
78
|
+
return True
|
|
79
|
+
except ValueError:
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
83
|
+
return f"The :name must be a valid IP address."
|
|
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."
|
|
97
|
+
|
|
98
|
+
class HexRule(ValidationRule):
|
|
99
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
100
|
+
if not isinstance(value, str):
|
|
101
|
+
return False
|
|
102
|
+
return bool(re.match(r'^#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', value))
|
|
103
|
+
|
|
104
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
105
|
+
return f"The :name must be a valid hexadecimal color code."
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from .base import ValidationRule
|
|
2
|
+
from typing import Any, Dict, List, Optional, Set, Union, Tuple, Type
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
# =============================================
|
|
6
|
+
# STRING VALIDATION RULES
|
|
7
|
+
# =============================================
|
|
8
|
+
|
|
9
|
+
class StringRule(ValidationRule):
|
|
10
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
11
|
+
return isinstance(value, str)
|
|
12
|
+
|
|
13
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
14
|
+
return f"The :name must be a string."
|
|
15
|
+
|
|
16
|
+
class AlphaRule(ValidationRule):
|
|
17
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
18
|
+
return isinstance(value, str) and value.isalpha()
|
|
19
|
+
|
|
20
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
21
|
+
return f"The :name may only contain letters."
|
|
22
|
+
|
|
23
|
+
class AlphaDashRule(ValidationRule):
|
|
24
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
25
|
+
return isinstance(value, str) and bool(re.match(r'^[a-zA-Z0-9_-]+$', value))
|
|
26
|
+
|
|
27
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
28
|
+
return f"The :name may only contain letters, numbers, dashes and underscores."
|
|
29
|
+
|
|
30
|
+
class AlphaNumRule(ValidationRule):
|
|
31
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
32
|
+
return isinstance(value, str) and value.isalnum()
|
|
33
|
+
|
|
34
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
35
|
+
return f"The :name may only contain letters and numbers."
|
|
36
|
+
|
|
37
|
+
class UppercaseRule(ValidationRule):
|
|
38
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
39
|
+
return isinstance(value, str) and value == value.upper()
|
|
40
|
+
|
|
41
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
42
|
+
return f"The :name must be uppercase."
|
|
43
|
+
|
|
44
|
+
class LowercaseRule(ValidationRule):
|
|
45
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
46
|
+
return isinstance(value, str) and value == value.lower()
|
|
47
|
+
|
|
48
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
49
|
+
return f"The :name must be lowercase."
|
|
50
|
+
|
|
51
|
+
class AsciiRule(ValidationRule):
|
|
52
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
53
|
+
return isinstance(value, str) and all(ord(c) < 128 for c in value)
|
|
54
|
+
|
|
55
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
56
|
+
return f"The :name must only contain ASCII characters."
|
|
57
|
+
|
|
58
|
+
class StartsWithRule(ValidationRule):
|
|
59
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
60
|
+
if not isinstance(value, str):
|
|
61
|
+
return False
|
|
62
|
+
return any(value.startswith(p) for p in params)
|
|
63
|
+
|
|
64
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
65
|
+
return f"The :name must start with one of the following: {', '.join(params)}."
|
|
66
|
+
|
|
67
|
+
class EndsWithRule(ValidationRule):
|
|
68
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
69
|
+
if not isinstance(value, str):
|
|
70
|
+
return False
|
|
71
|
+
return any(value.endswith(p) for p in params)
|
|
72
|
+
|
|
73
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
74
|
+
return f"The :name must end with one of the following: {', '.join(params)}."
|
validator/rules/type.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from .base import ValidationRule
|
|
2
|
+
from typing import Any, Dict, List, Optional, Set, Union, Tuple, Type
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
# =============================================
|
|
6
|
+
# TYPE DATA VALIDATION RULES
|
|
7
|
+
# =============================================
|
|
8
|
+
|
|
9
|
+
class NumericRule(ValidationRule):
|
|
10
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
11
|
+
if isinstance(value, (int, float)):
|
|
12
|
+
return True
|
|
13
|
+
if not isinstance(value, str):
|
|
14
|
+
return False
|
|
15
|
+
return value.replace('.', '', 1).isdigit()
|
|
16
|
+
|
|
17
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
18
|
+
return f"The :name must be a number."
|
|
19
|
+
|
|
20
|
+
class IntegerRule(ValidationRule):
|
|
21
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
22
|
+
if isinstance(value, int):
|
|
23
|
+
return True
|
|
24
|
+
if not isinstance(value, str):
|
|
25
|
+
return False
|
|
26
|
+
return value.isdigit()
|
|
27
|
+
|
|
28
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
29
|
+
return f"The :name must be an integer."
|
|
30
|
+
|
|
31
|
+
class BooleanRule(ValidationRule):
|
|
32
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
33
|
+
if isinstance(value, bool):
|
|
34
|
+
return True
|
|
35
|
+
if isinstance(value, str):
|
|
36
|
+
return value.lower() in ['true', 'false', '1', '0', 'yes', 'no', 'on', 'off']
|
|
37
|
+
if isinstance(value, int):
|
|
38
|
+
return value in [0, 1]
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
42
|
+
return f"The :name field must be true or false."
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
from .base import ValidationRule
|
|
2
|
+
from typing import Any, Dict, List, Optional, Set, Union, Tuple, Type
|
|
3
|
+
|
|
4
|
+
class AnyOfRule(ValidationRule):
|
|
5
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
6
|
+
if not params:
|
|
7
|
+
return False
|
|
8
|
+
return any(self.get_field_value(param, param) == value for param in params)
|
|
9
|
+
|
|
10
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
11
|
+
return f"The :name must be one of: {', '.join(params)}"
|
|
12
|
+
|
|
13
|
+
class ExcludeRule(ValidationRule):
|
|
14
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
15
|
+
return False # This rule is typically used to exclude fields from validation
|
|
16
|
+
|
|
17
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
18
|
+
return f"The :name field is excluded."
|
|
19
|
+
|
|
20
|
+
class ExcludeIfRule(ValidationRule):
|
|
21
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
22
|
+
conditions = [(f.strip(), v.strip()) for f, v in zip(params[::2], params[1::2])]
|
|
23
|
+
|
|
24
|
+
all_conditions_met = all(
|
|
25
|
+
self.get_field_value(f) == v
|
|
26
|
+
for f, v in conditions
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if all_conditions_met:
|
|
30
|
+
self.validator._is_exclude = True
|
|
31
|
+
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
35
|
+
return f"The :name field is excluded when {params[0]} is {params[1]}."
|
|
36
|
+
|
|
37
|
+
class ExcludeUnlessRule(ValidationRule):
|
|
38
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
39
|
+
conditions = [(f.strip(), v.strip()) for f, v in zip(params[::2], params[1::2])]
|
|
40
|
+
|
|
41
|
+
all_conditions_met = all(
|
|
42
|
+
self.get_field_value(f) == v
|
|
43
|
+
for f, v in conditions
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if not all_conditions_met:
|
|
47
|
+
self.validator._is_exclude = True
|
|
48
|
+
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
52
|
+
return f"The :name field is excluded unless {params[0]} is {params[1]}."
|
|
53
|
+
|
|
54
|
+
class ExcludeWithRule(ValidationRule):
|
|
55
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
56
|
+
if any(not self.is_empty(self.get_field_value(param, None)) for param in params):
|
|
57
|
+
self.validator._is_exclude = True
|
|
58
|
+
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
62
|
+
return f"The :name field is excluded when any of {', '.join(params)} is present."
|
|
63
|
+
|
|
64
|
+
class ExcludeWithoutRule(ValidationRule):
|
|
65
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
66
|
+
if any(self.is_empty(self.get_field_value(param, None)) for param in params):
|
|
67
|
+
self.validator._is_exclude = True
|
|
68
|
+
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
72
|
+
return f"The :name field is excluded when any of {', '.join(params)} is missing."
|
|
73
|
+
|
|
74
|
+
class MissingRule(ValidationRule):
|
|
75
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
76
|
+
return value is None
|
|
77
|
+
|
|
78
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
79
|
+
return f"The :name field must be missing."
|
|
80
|
+
|
|
81
|
+
class MissingIfRule(ValidationRule):
|
|
82
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
83
|
+
if len(params) < 2:
|
|
84
|
+
return False
|
|
85
|
+
other_field, other_value = params[0], params[1]
|
|
86
|
+
return value is None if self.get_field_value(other_field, None) == other_value else True
|
|
87
|
+
|
|
88
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
89
|
+
return f"The :name field must be missing when {params[0]} is {params[1]}."
|
|
90
|
+
|
|
91
|
+
class MissingUnlessRule(ValidationRule):
|
|
92
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
93
|
+
if len(params) < 2:
|
|
94
|
+
return False
|
|
95
|
+
other_field, other_value = params[0], params[1]
|
|
96
|
+
return value is None if self.get_field_value(other_field, None) != other_value else True
|
|
97
|
+
|
|
98
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
99
|
+
return f"The :name field must be missing unless {params[0]} is {params[1]}."
|
|
100
|
+
|
|
101
|
+
class MissingWithRule(ValidationRule):
|
|
102
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
103
|
+
if not params:
|
|
104
|
+
return False
|
|
105
|
+
return value is None if any(self.get_field_value(param, None) is not None for param in params) else True
|
|
106
|
+
|
|
107
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
108
|
+
return f"The :name field must be missing when any of {', '.join(params)} is present."
|
|
109
|
+
|
|
110
|
+
class MissingWithAllRule(ValidationRule):
|
|
111
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
112
|
+
if not params:
|
|
113
|
+
return False
|
|
114
|
+
return value is None if all(self.get_field_value(param, None) is not None for param in params) else True
|
|
115
|
+
|
|
116
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
117
|
+
return f"The :name field must be missing when all of {', '.join(params)} are present."
|
|
118
|
+
|
|
119
|
+
class PresentIfRule(ValidationRule):
|
|
120
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
121
|
+
if len(params) < 2:
|
|
122
|
+
return False
|
|
123
|
+
other_field, other_value = params[0], params[1]
|
|
124
|
+
return value is not None if self.get_field_value(other_field, None) == other_value else True
|
|
125
|
+
|
|
126
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
127
|
+
return f"The :name field must be present when {params[0]} is {params[1]}."
|
|
128
|
+
|
|
129
|
+
class PresentUnlessRule(ValidationRule):
|
|
130
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
131
|
+
if len(params) < 2:
|
|
132
|
+
return False
|
|
133
|
+
other_field, other_value = params[0], params[1]
|
|
134
|
+
return value is not None if self.get_field_value(other_field, None) != other_value else True
|
|
135
|
+
|
|
136
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
137
|
+
return f"The :name field must be present unless {params[0]} is {params[1]}."
|
|
138
|
+
|
|
139
|
+
class PresentWithRule(ValidationRule):
|
|
140
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
141
|
+
if not params:
|
|
142
|
+
return False
|
|
143
|
+
return value is not None if any(self.get_field_value(param, None) is not None for param in params) else True
|
|
144
|
+
|
|
145
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
146
|
+
return f"The :name field must be present when any of {', '.join(params)} is present."
|
|
147
|
+
|
|
148
|
+
class PresentWithAllRule(ValidationRule):
|
|
149
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
150
|
+
if not params:
|
|
151
|
+
return False
|
|
152
|
+
return value is not None if all(self.get_field_value(param, None) is not None for param in params) else True
|
|
153
|
+
|
|
154
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
155
|
+
return f"The :name field must be present when all of {', '.join(params)} are present."
|
|
156
|
+
|
|
157
|
+
class ProhibitedIfAcceptedRule(ValidationRule):
|
|
158
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
159
|
+
if not params:
|
|
160
|
+
return False
|
|
161
|
+
other_field = params[0]
|
|
162
|
+
return value is None if self.get_field_value(other_field, None) in ['yes', 'on', '1', 1, True, 'true', 'True'] else True
|
|
163
|
+
|
|
164
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
165
|
+
return f"The :name field is prohibited when {params[0]} is accepted."
|
|
166
|
+
|
|
167
|
+
class ProhibitedIfDeclinedRule(ValidationRule):
|
|
168
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
169
|
+
if not params:
|
|
170
|
+
return False
|
|
171
|
+
other_field = params[0]
|
|
172
|
+
return value is None if self.get_field_value(other_field, None) in ['no', 'off', '0', 0, False, 'false', 'False'] else True
|
|
173
|
+
|
|
174
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
175
|
+
return f"The :name field is prohibited when {params[0]} is declined."
|
|
176
|
+
|
|
177
|
+
class ProhibitsRule(ValidationRule):
|
|
178
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
179
|
+
if not params or value is None:
|
|
180
|
+
return True
|
|
181
|
+
return all(self.get_field_value(param, param) in (None, 'None') for param in params)
|
|
182
|
+
|
|
183
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
184
|
+
return f"When :name is present, {', '.join(params)} must be absent."
|
|
185
|
+
|
|
186
|
+
class RequiredIfAcceptedRule(ValidationRule):
|
|
187
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
188
|
+
if not params:
|
|
189
|
+
return False
|
|
190
|
+
other_field = params[0]
|
|
191
|
+
return value is not None if self.get_field_value(other_field, None) in ['yes', 'on', '1', 1, True, 'true', 'True'] else True
|
|
192
|
+
|
|
193
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
194
|
+
return f"The :name field is required when {params[0]} is accepted."
|
|
195
|
+
|
|
196
|
+
class RequiredIfDeclinedRule(ValidationRule):
|
|
197
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
198
|
+
if not params:
|
|
199
|
+
return False
|
|
200
|
+
other_field = params[0]
|
|
201
|
+
return value is not None if self.get_field_value(other_field, other_field) in ['no', 'off', '0', 0, False, 'false', 'False'] else True
|
|
202
|
+
|
|
203
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
204
|
+
return f"The :name field is required when {params[0]} is declined."
|
|
205
|
+
|
|
206
|
+
class RequiredArrayKeysRule(ValidationRule):
|
|
207
|
+
def validate(self, field: str, value: Any, params: List[str]) -> bool:
|
|
208
|
+
if not isinstance(value, dict) or not params:
|
|
209
|
+
return False
|
|
210
|
+
return all(key in value for key in params)
|
|
211
|
+
|
|
212
|
+
def message(self, field: str, params: List[str]) -> str:
|
|
213
|
+
return f"The :name must contain all required keys: {', '.join(params)}"
|
|
@@ -0,0 +1,133 @@
|
|
|
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['ValidationRule']) -> None:
|
|
62
|
+
"""Main method untuk mengecek semua jenis konflik.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
rules: List of ValidationRule 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
|
+
)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Dict, List, Any
|
|
2
|
+
|
|
3
|
+
class RuleErrorHandler:
|
|
4
|
+
"""Manages validation errors and message formatting"""
|
|
5
|
+
def __init__(self, messages: Dict[str, str], custom_attributes: Dict[str, str]):
|
|
6
|
+
self.messages = messages or {}
|
|
7
|
+
self.custom_attributes = custom_attributes or {}
|
|
8
|
+
self.errors: Dict[str, List[str]] = {}
|
|
9
|
+
|
|
10
|
+
def add_error(self, field: str, rule_name: str, default_message: str, value: Any):
|
|
11
|
+
"""Add formatted error message"""
|
|
12
|
+
message = self._format_message(field, rule_name, default_message, value)
|
|
13
|
+
|
|
14
|
+
# Handle nested field display names
|
|
15
|
+
self.errors.setdefault(field, []).append(message)
|
|
16
|
+
|
|
17
|
+
def _format_message(self, field: str, rule_name: str, default_message: str, value: Any) -> str:
|
|
18
|
+
"""Format error message with placeholders"""
|
|
19
|
+
# Get the base field name (last part of nested path)
|
|
20
|
+
base_field = field.split('.')[-1]
|
|
21
|
+
|
|
22
|
+
attribute = self.custom_attributes.get(field) or self.custom_attributes.get(base_field, field)
|
|
23
|
+
value_str = str(value) if value is not None else ''
|
|
24
|
+
|
|
25
|
+
message = (
|
|
26
|
+
self.messages.get(field) or
|
|
27
|
+
self.messages.get(attribute) or
|
|
28
|
+
self.messages.get(f"{attribute}.*") or
|
|
29
|
+
self.messages.get(f"{field}.{rule_name}") or
|
|
30
|
+
default_message
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return (message
|
|
34
|
+
.replace(':name', attribute)
|
|
35
|
+
.replace(':value', value_str)
|
|
36
|
+
.replace(':param', rule_name.split(':')[1] if ':' in rule_name else '')
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def has_errors(self) -> bool:
|
|
41
|
+
"""Check if any errors exist"""
|
|
42
|
+
return bool(self.errors)
|