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.

@@ -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)}."
@@ -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,5 @@
1
+ from .rule_preparer import RulePreparer
2
+ from .rule_error_handler import RuleErrorHandler
3
+ from .rule_conflict import RuleConflictChecker
4
+
5
+ __all__ = {'RulePreparer', 'RuleErrorHandler', 'RuleConflictChecker'}
@@ -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)