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.
@@ -1,41 +1,238 @@
1
- from typing import Dict, List, Any
1
+ from typing import Dict, List, Any, Optional, Union
2
+ import re
2
3
 
3
4
  class RuleErrorHandler:
4
- """Manages validation errors and message formatting"""
5
+ """Enhanced validation error handler with complete Laravel-style placeholder support,
6
+ including field-value pair parameters (field1,value1,field2,value2)"""
7
+
5
8
  def __init__(self, messages: Dict[str, str], custom_attributes: Dict[str, str]):
6
9
  self.messages = messages or {}
7
10
  self.custom_attributes = custom_attributes or {}
8
11
  self.errors: Dict[str, List[str]] = {}
12
+ self._current_rule: Optional[str] = None
13
+ self._current_params: Optional[List[str]] = None
14
+ self._current_value: Optional[Any] = None
9
15
 
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)
16
+ def add_error(self, field: str, rule_name: str, rule_params: List[str], default_message: str, value: Any) -> None:
17
+ """Add a formatted error message with complete placeholder support"""
18
+ self._current_rule = rule_name
19
+ self._current_params = rule_params
20
+ self._current_value = value
13
21
 
14
- # Handle nested field display names
22
+ message = self._format_message(field, rule_name, default_message, value)
15
23
  self.errors.setdefault(field, []).append(message)
16
24
 
17
25
  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
- )
26
+ """Format error message with all supported placeholders"""
27
+ attribute = self._get_attribute_name(field)
28
+ value_str = self._stringify_value(value)
29
+
30
+ # Get the most specific message available
31
+ message = self._get_message(field, rule_name, attribute, default_message)
32
+
33
+ # Prepare all possible replacements
34
+ replacements = self._prepare_replacements(attribute, value_str)
35
+
36
+ # Apply replacements safely
37
+ return self._apply_replacements(message, replacements)
38
+
39
+ def _get_attribute_name(self, field: str) -> str:
40
+ """Get the display name for a field with nested field support"""
41
+ # Check for exact match first
42
+ if field in self.custom_attributes:
43
+ return self.custom_attributes[field]
44
+
45
+ # Handle nested fields (e.g., 'user.profile.name')
46
+ parts = field.split('.')
47
+ for i in range(len(parts), 0, -1):
48
+ wildcard_key = '.'.join(parts[:i]) + '.*'
49
+ if wildcard_key in self.custom_attributes:
50
+ return self.custom_attributes[wildcard_key]
32
51
 
33
- return (message
34
- .replace(':name', attribute)
35
- .replace(':value', value_str)
36
- .replace(':param', rule_name.split(':')[1] if ':' in rule_name else '')
52
+ # Fallback to last part or field itself
53
+ return self.custom_attributes.get(parts[-1], field.replace('_', ' ').title())
54
+
55
+ def _stringify_value(self, value: Any) -> str:
56
+ """Convert any value to a string representation"""
57
+ if value is None:
58
+ return ''
59
+ if isinstance(value, (list, dict, set)):
60
+ return ', '.join(str(v) for v in value) if value else ''
61
+ return str(value)
62
+
63
+ def _get_message(self, field: str, rule_name: str, attribute: str, default: str) -> str:
64
+ """Get the most specific error message available"""
65
+ return (
66
+ self.messages.get(f"{field}.{rule_name}") or # Field-specific rule message
67
+ self.messages.get(field) or # Field-specific default
68
+ self.messages.get(rule_name) or # Rule-specific default
69
+ default # Fallback
37
70
  )
38
71
 
72
+ def _prepare_replacements(self, attribute: str, value_str: str) -> Dict[str, str]:
73
+ """Prepare all placeholder replacements including field-value pairs"""
74
+ replacements = {
75
+ ':attribute': attribute,
76
+ ':input': value_str,
77
+ ':value': value_str,
78
+ ':values': self._get_values_param(),
79
+ ':min': self._get_min_param(),
80
+ ':max': self._get_max_param(),
81
+ ':size': self._get_size_param(),
82
+ ':other': self._get_other_param_display(),
83
+ ':date': self._get_date_param(),
84
+ ':format': self._get_format_param(),
85
+ ':param': self._get_first_param(),
86
+ }
87
+
88
+ # Add numbered placeholders for field-value pairs (e.g., :other1, :value1, :other2, :value2)
89
+ if self._is_field_value_rule() and self._current_params:
90
+ field_value_pairs = self._get_field_value_pairs()
91
+ if field_value_pairs:
92
+ first_field, first_value = field_value_pairs[0]
93
+ replacements[':other'] = self._get_attribute_name(first_field)
94
+ replacements[':value'] = first_value
95
+
96
+ for i, (field, val) in enumerate(field_value_pairs[1:], start=2):
97
+ replacements[f':other{i}'] = self._get_attribute_name(field)
98
+ replacements[f':value{i}'] = val
99
+
100
+ return replacements
101
+
102
+ def _is_field_value_rule(self) -> bool:
103
+ """Check if the current rule uses field-value pairs"""
104
+ return self._current_rule and self._current_rule.lower() in {
105
+ 'required_if', 'required_unless',
106
+ 'exclude_if', 'exclude_unless',
107
+ 'missing_if', 'missing_unless',
108
+ 'present_if', 'present_unless'
109
+ }
110
+
111
+ def _get_field_value_pairs(self) -> List[tuple]:
112
+ """Extract field-value pairs from parameters"""
113
+ if not self._current_params:
114
+ return []
115
+
116
+ pairs = []
117
+ params = list(self._current_params).copy()
118
+
119
+ # Process parameters in pairs (field, value)
120
+ while len(params) >= 2:
121
+ field = params.pop(0)
122
+ value = params.pop(0)
123
+ pairs.append((field, value))
124
+
125
+ return pairs
126
+
127
+ def _apply_replacements(self, message: str, replacements: Dict[str, str]) -> str:
128
+ """Safely apply all replacements to the message"""
129
+ for placeholder, replacement in replacements.items():
130
+ if replacement is not None:
131
+ # Use regex to avoid partial replacements
132
+ message = re.sub(
133
+ re.escape(placeholder) + r'(?![a-zA-Z0-9_])',
134
+ str(replacement),
135
+ message
136
+ )
137
+ return message
138
+
139
+ def _get_other_param_display(self) -> Optional[str]:
140
+ """Get display names for other fields with proper formatting"""
141
+ other_fields = self._get_raw_other_fields()
142
+ if not other_fields:
143
+ return None
144
+
145
+ display_names = [self._get_attribute_name(f) for f in other_fields]
146
+
147
+ if len(display_names) == 1:
148
+ return display_names[0]
149
+ if len(display_names) == 2:
150
+ return f"{display_names[0]} and {display_names[1]}"
151
+ return f"{', '.join(display_names[:-1])}, and {display_names[-1]}"
152
+
153
+ def _get_raw_other_fields(self) -> List[str]:
154
+ """Extract field references from rule parameters"""
155
+ if not self._current_rule or not self._current_params:
156
+ return []
157
+
158
+ rule = self._current_rule.lower()
159
+
160
+ # Rules with field-value pairs (field1,value1,field2,value2,...)
161
+ if rule in {
162
+ 'required_if', 'required_unless', 'exclude_if', 'exclude_unless',
163
+ 'missing_if', 'missing_unless', 'present_if', 'present_unless'
164
+ }:
165
+ return self._current_params[::2] # Take every even index
166
+
167
+ # Rules with just field references
168
+ if rule in {
169
+ 'prohibits', 'exclude_with', 'exclude_without',
170
+ 'missing_with', 'missing_with_all',
171
+ 'present_with', 'present_with_all'
172
+ }:
173
+ return self._current_params
174
+
175
+ # Single field rules
176
+ if rule in {
177
+ 'required_if_accepted', 'required_if_declined',
178
+ 'prohibited_if_accepted', 'prohibited_if_declined'
179
+ }:
180
+ return [self._current_params[0]] if self._current_params else []
181
+
182
+ return []
183
+
184
+ def _get_min_param(self) -> Optional[str]:
185
+ """Get min parameter from rule"""
186
+ if not self._current_params:
187
+ return None
188
+
189
+ if self._current_rule and self._current_rule.startswith(('min', 'between', 'digits_between')):
190
+ return self._current_params[0]
191
+ return None
192
+
193
+ def _get_max_param(self) -> Optional[str]:
194
+ """Get max parameter from rule"""
195
+ if not self._current_params or len(self._current_params) < 2:
196
+ return None
197
+
198
+ if self._current_rule and self._current_rule.startswith(('max', 'between', 'digits_between')):
199
+ return self._current_params[1] if self._current_rule.startswith('between') else self._current_params[0]
200
+ return None
201
+
202
+ def _get_size_param(self) -> Optional[str]:
203
+ """Get size parameter from rule"""
204
+ if self._current_rule and self._current_rule.startswith('size') and self._current_params:
205
+ return self._current_params[0]
206
+ return None
207
+
208
+ def _get_values_param(self) -> Optional[str]:
209
+ """Get values list for in/not_in rules"""
210
+ if (self._current_rule and
211
+ self._current_rule.startswith(('in', 'not_in')) and
212
+ self._current_params):
213
+ return ', '.join(self._current_params)
214
+ return None
215
+
216
+ def _get_date_param(self) -> Optional[str]:
217
+ """Get date parameter for date rules"""
218
+ if (self._current_rule and
219
+ self._current_rule.startswith(('after', 'before', 'after_or_equal', 'before_or_equal')) and
220
+ self._current_params):
221
+ return self._current_params[0]
222
+ return None
223
+
224
+ def _get_format_param(self) -> Optional[str]:
225
+ """Get format parameter"""
226
+ if (self._current_rule and
227
+ self._current_rule.startswith('date_format') and
228
+ self._current_params):
229
+ return self._current_params[0]
230
+ return None
231
+
232
+ def _get_first_param(self) -> Optional[str]:
233
+ """Get first parameter from rule"""
234
+ return self._current_params[0] if self._current_params else None
235
+
39
236
  @property
40
237
  def has_errors(self) -> bool:
41
238
  """Check if any errors exist"""
@@ -1,6 +1,6 @@
1
1
  from validator.factory import RuleFactory
2
2
  from validator.services.rule_conflict import RuleConflictChecker
3
- from validator.rules import ValidationRule
3
+ from validator.rules import Rule
4
4
  from typing import Dict, List, Union, Tuple
5
5
  from validator.exceptions import RuleNotFoundException
6
6
  from collections.abc import Iterable, Sequence
@@ -14,7 +14,7 @@ class RulePreparer:
14
14
  def __init__(self, rule_factory: RuleFactory):
15
15
  self.rule_factory = rule_factory
16
16
 
17
- def prepare(self, raw_rules: Dict[str, Union[str, List[Union[str, ValidationRule]]]]) -> Dict[str, List[ValidationRule]]:
17
+ def prepare(self, raw_rules: Dict[str, Union[str, List[Union[str, Rule]]]]) -> Dict[str, List[Rule]]:
18
18
  """Convert raw rules to prepared validation rules"""
19
19
  prepared_rules = {}
20
20
  for field, rule_input in raw_rules.items():
@@ -25,18 +25,18 @@ class RulePreparer:
25
25
  prepared_rules[field] = self._deduplicate_rules(rules)
26
26
  return prepared_rules
27
27
 
28
- def _convert_to_rules(self, rule_input: Union[str, List[Union[str, ValidationRule]], ValidationRule, Tuple[Union[str, ValidationRule], str], Tuple[Union[str, ValidationRule], str]]) -> List[ValidationRule]:
29
- """Convert mixed rule input to list of ValidationRule objects"""
28
+ def _convert_to_rules(self, rule_input: Union[str, List[Union[str, Rule]], Rule, Tuple[Union[str, Rule], str], Tuple[Union[str, Rule], str]]) -> List[Rule]:
29
+ """Convert mixed rule input to list of Rule objects"""
30
30
  if rule_input is None:
31
31
  return []
32
- if isinstance(rule_input, ValidationRule):
32
+ if isinstance(rule_input, Rule):
33
33
  return [rule_input]
34
- if isinstance(rule_input, list):
34
+ if isinstance(rule_input, list | tuple):
35
35
  rules = []
36
36
  for r in rule_input:
37
37
  if isinstance(r, str):
38
38
  rules.extend(self._parse_rule_string(r))
39
- elif isinstance(r, ValidationRule):
39
+ elif isinstance(r, Rule):
40
40
  rules.append(r)
41
41
  elif isinstance(r, (tuple, list)):
42
42
  if len(r) == 0:
@@ -50,10 +50,10 @@ class RulePreparer:
50
50
  if isinstance(rule_name, str):
51
51
  rule = RuleFactory.create_rule(rule_name)
52
52
  rule.params = params
53
- elif isinstance(rule_name, ValidationRule):
53
+ elif isinstance(rule_name, Rule):
54
54
  rule = rule_name
55
55
  if params:
56
- warnings.warn(f"Parameters {params} are ignored for ValidationRule instance")
56
+ warnings.warn(f"Parameters {params} are ignored for Rule instance")
57
57
  else:
58
58
  raise ValueError(f"Invalid rule name type in {r}")
59
59
 
@@ -65,8 +65,8 @@ class RulePreparer:
65
65
  return self._parse_rule_string(rule_input)
66
66
  raise ValueError(f"Invalid rule input type: {type(rule_input)}")
67
67
 
68
- def _parse_rule_string(self, rule_str: str) -> List[ValidationRule]:
69
- """Parse rule string into ValidationRule objects"""
68
+ def _parse_rule_string(self, rule_str: str) -> List[Rule]:
69
+ """Parse rule string into Rule objects"""
70
70
  rules = []
71
71
  for rule_part in rule_str.split('|'):
72
72
  rule_part = rule_part.strip()
@@ -93,22 +93,9 @@ class RulePreparer:
93
93
  return rule_name, [p.strip() for p in param_str.split(',') if p.strip()]
94
94
 
95
95
  def _parse_params(self, params: Union[Tuple, List, str, Enum]):
96
- all_params = []
97
- # seen = set()
98
- # for param in params:
99
- # if isinstance(param, Iterable) and not isinstance(param, str | bytes) and not inspect.isclass(param):
100
- # for item in param:
101
- # if item not in seen and item not in (None, '', [], {}) and (isinstance(item, (Number, Sequence)) or inspect.isclass(item)):
102
- # seen.add(item)
103
- # all_params.append(item)
104
- # else:
105
- # if param not in seen and param not in (None, '', [], {}) and (isinstance(param, (Number, Sequence)) or inspect.isclass(param)):
106
- # seen.add(param)
107
- # all_params.append(param)
108
-
109
96
  return set(tuple(x) if isinstance(x, list) else x for x in params)
110
97
 
111
- def _deduplicate_rules(self, rules: List[ValidationRule]) -> List[ValidationRule]:
98
+ def _deduplicate_rules(self, rules: List[Rule]) -> List[Rule]:
112
99
  """Remove duplicate rules based on name and parameters"""
113
100
  seen = set()
114
101
  unique = []
@@ -1,31 +0,0 @@
1
- validator/__init__.py,sha256=udxDzUicPfxBOAQvzsnl3pHur9VUppKbWMgg35hpiww,244
2
- validator/exceptions.py,sha256=y2v7CaXmeGFHWcnigtLl4U-sFta_jMiXkGKXWIIVglY,366
3
- validator/factory.py,sha256=bImQNLhEJg5VTxtHiMYVb2EbHWvimTDqHq-UcA8uolw,812
4
- validator/core/__init__.py,sha256=ZcqlXJSk03i_CVzmIN-nVe1UOyvwwO5jhbEj7f62Y_o,59
5
- validator/core/validator.py,sha256=00qVnbH-EJC5KALlaoUBLAfsszAFLcoSxfRbmy0amyk,12751
6
- validator/database/__init__.py,sha256=O-cB6-MhNapJ3iwe5jvifbMfr1dPjXLtEdfNTKIu0hc,171
7
- validator/database/detector.py,sha256=Vac7oVL26GjU6expGo01-6mgUtXqldr-jirzpYokZBM,9597
8
- validator/database/manager.py,sha256=XJM_I0WaWfZWV710duAc32p1gtiP2or-MAj75WPw1oM,6478
9
- validator/rules/__init__.py,sha256=nDE3qoI82qJTCbILLUWkXuwsMOmsDtB1m-3IGIvRfpY,919
10
- validator/rules/array.py,sha256=tx8FCDqn-27Vs7tgtjeoCE9ceDMVrdBEb2-pq-lNLuo,2809
11
- validator/rules/base.py,sha256=hrGESfkdvqZrQ1yIK_ftVVOMUuyvNFoz-_qaqUucSy8,3474
12
- validator/rules/basic.py,sha256=fMk0s5_IEQu27X1wndP_ocNyOZ1upXJCn7fR9YYbI74,1527
13
- validator/rules/comparison.py,sha256=9xb2mO5GiThR1iO8867ex2o7olQC2Bnew6MBdD2UtEo,9402
14
- validator/rules/conditional.py,sha256=8_O1etzCyCGeD8lmfhegJ1uzhkBSjTEVdufbZmkqgP4,12993
15
- validator/rules/date.py,sha256=18JIKTO5nzFtCnJEMXm4OUbneqTMB7HGPN6jUkxGU4Y,5278
16
- validator/rules/files.py,sha256=vu_TZFffDPzDojyTsFXSQ6MmQm3WU6ppFfz7-wuDy98,6550
17
- validator/rules/format.py,sha256=UeeIAkFn0CdzC5bS9tI78_lbPYRgcpaUujxdmyCSdzE,3744
18
- validator/rules/string.py,sha256=vYzu4ICKY9FCuGahmsQCoJLmnlBF7uNvVazFj9DQ438,3178
19
- validator/rules/type.py,sha256=Tu-EOBkTtxkcCe0ANXavurZC449n63iE_VXVzc3BIiM,1596
20
- validator/rules/utilities.py,sha256=AIm9JRGYf6cTCSkivg3gTj3U5DnXlCAJ5ej1yUSa1dU,9724
21
- validator/services/__init__.py,sha256=zzKTmqL7v4niFGWHJBfWLqgJ0iTaW_69OzYZN8uInzQ,210
22
- validator/services/rule_conflict.py,sha256=s1RJNUY5d0WtSMHkrKulBCgJ2BZL2GE0Eu5pdAoiIbM,4943
23
- validator/services/rule_error_handler.py,sha256=MGvvkP6hbZLpVXxC3xpzg15OmVdPlk7l0M2Srmy5VfM,1729
24
- validator/services/rule_preparer.py,sha256=jRcMNjqq2xyZjO64Pim8jWmja5DmTzf0V_uuHG0lJTg,5621
25
- validator/utils/__init__.py,sha256=Yzo-xv285Be-a233M4duDdYtscuHiuBbPSX_C8yViJI,20
26
- validator/utils/string.py,sha256=0YACzeEaWNEOR9_7O9A8D1ItIbtWfOJ8IfrzcB8VMYA,515
27
- safeshield-1.2.2.dist-info/LICENSE,sha256=qugtRyKckyaks6hd2xyxOFSOYM6au1N80pMXuMTPvC4,1090
28
- safeshield-1.2.2.dist-info/METADATA,sha256=Da-A5dUfizYqEpszV6KlIwRLJLgKj7KThQ_990k2wNg,1912
29
- safeshield-1.2.2.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
30
- safeshield-1.2.2.dist-info/top_level.txt,sha256=iUtV3dlHOIiMfLuY4pruY00lFni8JzOkQ3Nh1II19OE,10
31
- safeshield-1.2.2.dist-info/RECORD,,