socialseed-e2e 0.1.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.
- socialseed_e2e/__init__.py +51 -0
- socialseed_e2e/__version__.py +21 -0
- socialseed_e2e/cli.py +611 -0
- socialseed_e2e/core/__init__.py +35 -0
- socialseed_e2e/core/base_page.py +839 -0
- socialseed_e2e/core/check_deps.py +43 -0
- socialseed_e2e/core/config.py +119 -0
- socialseed_e2e/core/config_loader.py +604 -0
- socialseed_e2e/core/headers.py +20 -0
- socialseed_e2e/core/interfaces.py +22 -0
- socialseed_e2e/core/loaders.py +51 -0
- socialseed_e2e/core/models.py +24 -0
- socialseed_e2e/core/test_orchestrator.py +84 -0
- socialseed_e2e/services/__init__.py +9 -0
- socialseed_e2e/templates/__init__.py +32 -0
- socialseed_e2e/templates/config.py.template +20 -0
- socialseed_e2e/templates/data_schema.py.template +116 -0
- socialseed_e2e/templates/e2e.conf.template +20 -0
- socialseed_e2e/templates/service_page.py.template +83 -0
- socialseed_e2e/templates/test_module.py.template +46 -0
- socialseed_e2e/utils/__init__.py +44 -0
- socialseed_e2e/utils/template_engine.py +246 -0
- socialseed_e2e/utils/validators.py +588 -0
- socialseed_e2e-0.1.0.dist-info/METADATA +333 -0
- socialseed_e2e-0.1.0.dist-info/RECORD +29 -0
- socialseed_e2e-0.1.0.dist-info/WHEEL +5 -0
- socialseed_e2e-0.1.0.dist-info/entry_points.txt +3 -0
- socialseed_e2e-0.1.0.dist-info/licenses/LICENSE +21 -0
- socialseed_e2e-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
"""Validation helpers for socialseed-e2e.
|
|
2
|
+
|
|
3
|
+
This module provides validation functions for common use cases including
|
|
4
|
+
URL validation, configuration validation, data type validation, and
|
|
5
|
+
response validation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any, Callable, Dict, List, Optional, Pattern, Union
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ValidationError(Exception):
|
|
14
|
+
"""Exception raised when validation fails.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
message: Explanation of the validation error
|
|
18
|
+
field: Name of the field that failed validation (if applicable)
|
|
19
|
+
value: The value that failed validation (if applicable)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, message: str, field: Optional[str] = None, value: Any = None):
|
|
23
|
+
super().__init__(message)
|
|
24
|
+
self.message = message
|
|
25
|
+
self.field = field
|
|
26
|
+
self.value = value
|
|
27
|
+
|
|
28
|
+
def __str__(self) -> str:
|
|
29
|
+
if self.field:
|
|
30
|
+
return f"Validation error for '{self.field}': {self.message}"
|
|
31
|
+
return f"Validation error: {self.message}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# URL Validation
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def validate_url(
|
|
38
|
+
url: str,
|
|
39
|
+
require_scheme: bool = True,
|
|
40
|
+
allowed_schemes: Optional[List[str]] = None,
|
|
41
|
+
field_name: str = "url",
|
|
42
|
+
) -> str:
|
|
43
|
+
"""Validate a URL string.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
url: The URL to validate
|
|
47
|
+
require_scheme: Whether the URL must have a scheme (http/https)
|
|
48
|
+
allowed_schemes: List of allowed schemes (default: ['http', 'https'])
|
|
49
|
+
field_name: Name of the field for error messages
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
The validated URL string
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
ValidationError: If URL is invalid
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
>>> validate_url("https://api.example.com")
|
|
59
|
+
'https://api.example.com'
|
|
60
|
+
>>> validate_url("not-a-url")
|
|
61
|
+
ValidationError: Invalid URL format
|
|
62
|
+
"""
|
|
63
|
+
if not url or not isinstance(url, str):
|
|
64
|
+
raise ValidationError("URL must be a non-empty string", field_name, url)
|
|
65
|
+
|
|
66
|
+
url = url.strip()
|
|
67
|
+
|
|
68
|
+
# Check for spaces
|
|
69
|
+
if " " in url:
|
|
70
|
+
raise ValidationError("URL cannot contain spaces", field_name, url)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
parsed = urlparse(url)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
raise ValidationError(f"Invalid URL format: {e}", field_name, url)
|
|
76
|
+
|
|
77
|
+
# Check scheme
|
|
78
|
+
if require_scheme:
|
|
79
|
+
if not parsed.scheme:
|
|
80
|
+
raise ValidationError("URL must include scheme (http/https)", field_name, url)
|
|
81
|
+
|
|
82
|
+
allowed = allowed_schemes or ["http", "https"]
|
|
83
|
+
if parsed.scheme not in allowed:
|
|
84
|
+
raise ValidationError(
|
|
85
|
+
f"URL scheme '{parsed.scheme}' not allowed. Allowed: {', '.join(allowed)}",
|
|
86
|
+
field_name,
|
|
87
|
+
url,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Check netloc (domain)
|
|
91
|
+
# If no netloc and no scheme, try parsing with a dummy scheme
|
|
92
|
+
if not parsed.netloc and not parsed.scheme:
|
|
93
|
+
parsed_with_scheme = urlparse("http://" + url)
|
|
94
|
+
if parsed_with_scheme.netloc:
|
|
95
|
+
parsed = parsed_with_scheme
|
|
96
|
+
|
|
97
|
+
if not parsed.netloc:
|
|
98
|
+
raise ValidationError("URL must include a domain", field_name, url)
|
|
99
|
+
|
|
100
|
+
# Validate domain format
|
|
101
|
+
domain = parsed.netloc.split(":")[0] # Remove port if present
|
|
102
|
+
if not re.match(r"^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$", domain):
|
|
103
|
+
raise ValidationError(f"Invalid domain format: {domain}", field_name, url)
|
|
104
|
+
|
|
105
|
+
return url
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def validate_base_url(base_url: str, field_name: str = "base_url") -> str:
|
|
109
|
+
"""Validate a base URL for API services.
|
|
110
|
+
|
|
111
|
+
Similar to validate_url but specifically for API base URLs,
|
|
112
|
+
ensuring no trailing slash for consistent endpoint construction.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
base_url: The base URL to validate
|
|
116
|
+
field_name: Name of the field for error messages
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
The validated base URL (without trailing slash)
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
ValidationError: If base URL is invalid
|
|
123
|
+
"""
|
|
124
|
+
url = validate_url(base_url, field_name=field_name)
|
|
125
|
+
# Remove trailing slash for consistent endpoint construction
|
|
126
|
+
return url.rstrip("/")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# Configuration Validation
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def validate_port(port: Union[int, str], field_name: str = "port") -> int:
|
|
133
|
+
"""Validate a port number.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
port: Port number to validate
|
|
137
|
+
field_name: Name of the field for error messages
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Validated port number as integer
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
ValidationError: If port is invalid
|
|
144
|
+
"""
|
|
145
|
+
try:
|
|
146
|
+
port_int = int(port)
|
|
147
|
+
except (ValueError, TypeError):
|
|
148
|
+
raise ValidationError(f"Port must be a valid integer, got: {port}", field_name, port)
|
|
149
|
+
|
|
150
|
+
if port_int < 1 or port_int > 65535:
|
|
151
|
+
raise ValidationError(
|
|
152
|
+
f"Port must be between 1 and 65535, got: {port_int}", field_name, port
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return port_int
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def validate_timeout(timeout: Union[int, str], field_name: str = "timeout") -> int:
|
|
159
|
+
"""Validate a timeout value in milliseconds.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
timeout: Timeout in milliseconds
|
|
163
|
+
field_name: Name of the field for error messages
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Validated timeout as integer
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
ValidationError: If timeout is invalid
|
|
170
|
+
"""
|
|
171
|
+
try:
|
|
172
|
+
timeout_int = int(timeout)
|
|
173
|
+
except (ValueError, TypeError):
|
|
174
|
+
raise ValidationError(
|
|
175
|
+
f"Timeout must be a valid integer, got: {timeout}", field_name, timeout
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if timeout_int <= 0:
|
|
179
|
+
raise ValidationError(f"Timeout must be positive, got: {timeout_int}", field_name, timeout)
|
|
180
|
+
|
|
181
|
+
if timeout_int < 100:
|
|
182
|
+
# Warning for very short timeouts, but still valid
|
|
183
|
+
pass # Could log warning here
|
|
184
|
+
|
|
185
|
+
return timeout_int
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def validate_service_name(name: str, field_name: str = "service_name") -> str:
|
|
189
|
+
"""Validate a service name.
|
|
190
|
+
|
|
191
|
+
Service names should be:
|
|
192
|
+
- Alphanumeric with hyphens and underscores
|
|
193
|
+
- Start with letter
|
|
194
|
+
- Between 1 and 50 characters
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
name: Service name to validate
|
|
198
|
+
field_name: Name of the field for error messages
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Validated service name
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
ValidationError: If name is invalid
|
|
205
|
+
"""
|
|
206
|
+
if not name or not isinstance(name, str):
|
|
207
|
+
raise ValidationError("Service name must be a non-empty string", field_name, name)
|
|
208
|
+
|
|
209
|
+
name = name.strip()
|
|
210
|
+
|
|
211
|
+
if len(name) < 1 or len(name) > 50:
|
|
212
|
+
raise ValidationError(
|
|
213
|
+
f"Service name must be between 1 and 50 characters, got: {len(name)}", field_name, name
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
if not re.match(r"^[a-zA-Z][a-zA-Z0-9_-]*$", name):
|
|
217
|
+
raise ValidationError(
|
|
218
|
+
"Service name must start with letter and contain only letters, numbers, hyphens, and underscores",
|
|
219
|
+
field_name,
|
|
220
|
+
name,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
return name
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# Data Type Validation
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def validate_string(
|
|
230
|
+
value: Any,
|
|
231
|
+
field_name: str = "value",
|
|
232
|
+
min_length: Optional[int] = None,
|
|
233
|
+
max_length: Optional[int] = None,
|
|
234
|
+
pattern: Optional[Union[str, Pattern]] = None,
|
|
235
|
+
allow_blank: bool = True,
|
|
236
|
+
) -> str:
|
|
237
|
+
"""Validate a string value.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
value: Value to validate
|
|
241
|
+
field_name: Name of the field for error messages
|
|
242
|
+
min_length: Minimum string length (optional)
|
|
243
|
+
max_length: Maximum string length (optional)
|
|
244
|
+
pattern: Regex pattern to match (optional)
|
|
245
|
+
allow_blank: Whether empty strings are allowed
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Validated string
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
ValidationError: If value is invalid
|
|
252
|
+
"""
|
|
253
|
+
if not isinstance(value, str):
|
|
254
|
+
raise ValidationError(f"Expected string, got {type(value).__name__}", field_name, value)
|
|
255
|
+
|
|
256
|
+
if not allow_blank and not value.strip():
|
|
257
|
+
raise ValidationError("String cannot be blank", field_name, value)
|
|
258
|
+
|
|
259
|
+
if min_length is not None and len(value) < min_length:
|
|
260
|
+
raise ValidationError(
|
|
261
|
+
f"String must be at least {min_length} characters, got: {len(value)}", field_name, value
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
if max_length is not None and len(value) > max_length:
|
|
265
|
+
raise ValidationError(
|
|
266
|
+
f"String must be at most {max_length} characters, got: {len(value)}", field_name, value
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
if pattern is not None:
|
|
270
|
+
if isinstance(pattern, str):
|
|
271
|
+
pattern = re.compile(pattern)
|
|
272
|
+
if not pattern.match(value):
|
|
273
|
+
raise ValidationError(
|
|
274
|
+
f"String does not match required pattern: {pattern.pattern}", field_name, value
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
return value
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def validate_integer(
|
|
281
|
+
value: Any,
|
|
282
|
+
field_name: str = "value",
|
|
283
|
+
min_value: Optional[int] = None,
|
|
284
|
+
max_value: Optional[int] = None,
|
|
285
|
+
) -> int:
|
|
286
|
+
"""Validate an integer value.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
value: Value to validate
|
|
290
|
+
field_name: Name of the field for error messages
|
|
291
|
+
min_value: Minimum allowed value (optional)
|
|
292
|
+
max_value: Maximum allowed value (optional)
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Validated integer
|
|
296
|
+
|
|
297
|
+
Raises:
|
|
298
|
+
ValidationError: If value is invalid
|
|
299
|
+
"""
|
|
300
|
+
try:
|
|
301
|
+
int_value = int(value)
|
|
302
|
+
except (ValueError, TypeError):
|
|
303
|
+
raise ValidationError(
|
|
304
|
+
f"Expected integer, got {type(value).__name__}: {value}", field_name, value
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
if min_value is not None and int_value < min_value:
|
|
308
|
+
raise ValidationError(
|
|
309
|
+
f"Value must be at least {min_value}, got: {int_value}", field_name, value
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
if max_value is not None and int_value > max_value:
|
|
313
|
+
raise ValidationError(
|
|
314
|
+
f"Value must be at most {max_value}, got: {int_value}", field_name, value
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
return int_value
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def validate_email(email: str, field_name: str = "email") -> str:
|
|
321
|
+
"""Validate an email address.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
email: Email address to validate
|
|
325
|
+
field_name: Name of the field for error messages
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Validated email address
|
|
329
|
+
|
|
330
|
+
Raises:
|
|
331
|
+
ValidationError: If email is invalid
|
|
332
|
+
"""
|
|
333
|
+
if not email or not isinstance(email, str):
|
|
334
|
+
raise ValidationError("Email must be a non-empty string", field_name, email)
|
|
335
|
+
|
|
336
|
+
email = email.strip()
|
|
337
|
+
|
|
338
|
+
# Basic email pattern
|
|
339
|
+
pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
|
340
|
+
if not re.match(pattern, email):
|
|
341
|
+
raise ValidationError("Invalid email format", field_name, email)
|
|
342
|
+
|
|
343
|
+
return email.lower()
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def validate_uuid(uuid_str: str, field_name: str = "uuid") -> str:
|
|
347
|
+
"""Validate a UUID string.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
uuid_str: UUID string to validate
|
|
351
|
+
field_name: Name of the field for error messages
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
Validated UUID string
|
|
355
|
+
|
|
356
|
+
Raises:
|
|
357
|
+
ValidationError: If UUID is invalid
|
|
358
|
+
"""
|
|
359
|
+
if not uuid_str or not isinstance(uuid_str, str):
|
|
360
|
+
raise ValidationError("UUID must be a non-empty string", field_name, uuid_str)
|
|
361
|
+
|
|
362
|
+
uuid_str = uuid_str.strip()
|
|
363
|
+
|
|
364
|
+
# UUID pattern (accepts both with and without hyphens)
|
|
365
|
+
pattern = r"^[0-9a-fA-F]{8}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{12}$"
|
|
366
|
+
if not re.match(pattern, uuid_str):
|
|
367
|
+
raise ValidationError("Invalid UUID format", field_name, uuid_str)
|
|
368
|
+
|
|
369
|
+
return uuid_str
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# Response Validation
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def validate_status_code(
|
|
376
|
+
status: int, expected: Union[int, List[int]], field_name: str = "status_code"
|
|
377
|
+
) -> int:
|
|
378
|
+
"""Validate an HTTP status code.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
status: Actual status code
|
|
382
|
+
expected: Expected status code(s)
|
|
383
|
+
field_name: Name of the field for error messages
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
Validated status code
|
|
387
|
+
|
|
388
|
+
Raises:
|
|
389
|
+
ValidationError: If status doesn't match expected
|
|
390
|
+
"""
|
|
391
|
+
if isinstance(expected, int):
|
|
392
|
+
expected = [expected]
|
|
393
|
+
|
|
394
|
+
if status not in expected:
|
|
395
|
+
raise ValidationError(f"Expected status code {expected}, got: {status}", field_name, status)
|
|
396
|
+
|
|
397
|
+
return status
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def validate_json_response(
|
|
401
|
+
data: Any,
|
|
402
|
+
required_fields: Optional[List[str]] = None,
|
|
403
|
+
field_types: Optional[Dict[str, type]] = None,
|
|
404
|
+
field_name: str = "response",
|
|
405
|
+
) -> Dict[str, Any]:
|
|
406
|
+
"""Validate a JSON response structure.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
data: JSON data to validate
|
|
410
|
+
required_fields: List of required field names
|
|
411
|
+
field_types: Dictionary of field names to expected types
|
|
412
|
+
field_name: Name of the field for error messages
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
Validated data as dictionary
|
|
416
|
+
|
|
417
|
+
Raises:
|
|
418
|
+
ValidationError: If data structure is invalid
|
|
419
|
+
"""
|
|
420
|
+
if not isinstance(data, dict):
|
|
421
|
+
raise ValidationError(
|
|
422
|
+
f"Expected JSON object (dict), got {type(data).__name__}", field_name, data
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
# Check required fields
|
|
426
|
+
if required_fields:
|
|
427
|
+
missing = [f for f in required_fields if f not in data]
|
|
428
|
+
if missing:
|
|
429
|
+
raise ValidationError(
|
|
430
|
+
f"Missing required fields: {', '.join(missing)}", field_name, data
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
# Validate field types
|
|
434
|
+
if field_types:
|
|
435
|
+
for field, expected_type in field_types.items():
|
|
436
|
+
if field in data:
|
|
437
|
+
actual_value = data[field]
|
|
438
|
+
if actual_value is not None and not isinstance(actual_value, expected_type):
|
|
439
|
+
raise ValidationError(
|
|
440
|
+
f"Field '{field}' must be {expected_type.__name__}, "
|
|
441
|
+
f"got {type(actual_value).__name__}",
|
|
442
|
+
field_name,
|
|
443
|
+
data,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
return data
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def validate_pagination_response(
|
|
450
|
+
data: Dict[str, Any],
|
|
451
|
+
items_field: str = "items",
|
|
452
|
+
total_field: str = "total",
|
|
453
|
+
page_field: str = "page",
|
|
454
|
+
field_name: str = "pagination",
|
|
455
|
+
) -> Dict[str, Any]:
|
|
456
|
+
"""Validate a paginated response structure.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
data: Response data to validate
|
|
460
|
+
items_field: Name of the items field
|
|
461
|
+
total_field: Name of the total count field
|
|
462
|
+
page_field: Name of the page field
|
|
463
|
+
field_name: Name of the field for error messages
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
Validated data
|
|
467
|
+
|
|
468
|
+
Raises:
|
|
469
|
+
ValidationError: If pagination structure is invalid
|
|
470
|
+
"""
|
|
471
|
+
if not isinstance(data, dict):
|
|
472
|
+
raise ValidationError("Pagination response must be a JSON object", field_name, data)
|
|
473
|
+
|
|
474
|
+
# Validate items field
|
|
475
|
+
if items_field not in data:
|
|
476
|
+
raise ValidationError(
|
|
477
|
+
f"Missing '{items_field}' field in pagination response", field_name, data
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
if not isinstance(data[items_field], list):
|
|
481
|
+
raise ValidationError(f"'{items_field}' must be a list", field_name, data)
|
|
482
|
+
|
|
483
|
+
# Validate total field
|
|
484
|
+
if total_field in data:
|
|
485
|
+
try:
|
|
486
|
+
validate_integer(data[total_field], total_field, min_value=0)
|
|
487
|
+
except ValidationError as e:
|
|
488
|
+
raise ValidationError(str(e), field_name, data)
|
|
489
|
+
|
|
490
|
+
# Validate page field
|
|
491
|
+
if page_field in data:
|
|
492
|
+
try:
|
|
493
|
+
validate_integer(data[page_field], page_field, min_value=1)
|
|
494
|
+
except ValidationError as e:
|
|
495
|
+
raise ValidationError(str(e), field_name, data)
|
|
496
|
+
|
|
497
|
+
return data
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
# Utility Validators
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def validate_list(
|
|
504
|
+
value: Any,
|
|
505
|
+
field_name: str = "value",
|
|
506
|
+
min_length: Optional[int] = None,
|
|
507
|
+
max_length: Optional[int] = None,
|
|
508
|
+
item_validator: Optional[Callable[[Any], None]] = None,
|
|
509
|
+
) -> List[Any]:
|
|
510
|
+
"""Validate a list value.
|
|
511
|
+
|
|
512
|
+
Args:
|
|
513
|
+
value: Value to validate
|
|
514
|
+
field_name: Name of the field for error messages
|
|
515
|
+
min_length: Minimum list length (optional)
|
|
516
|
+
max_length: Maximum list length (optional)
|
|
517
|
+
item_validator: Optional function to validate each item
|
|
518
|
+
|
|
519
|
+
Returns:
|
|
520
|
+
Validated list
|
|
521
|
+
|
|
522
|
+
Raises:
|
|
523
|
+
ValidationError: If value is invalid
|
|
524
|
+
"""
|
|
525
|
+
if not isinstance(value, list):
|
|
526
|
+
raise ValidationError(f"Expected list, got {type(value).__name__}", field_name, value)
|
|
527
|
+
|
|
528
|
+
if min_length is not None and len(value) < min_length:
|
|
529
|
+
raise ValidationError(
|
|
530
|
+
f"List must have at least {min_length} items, got: {len(value)}", field_name, value
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
if max_length is not None and len(value) > max_length:
|
|
534
|
+
raise ValidationError(
|
|
535
|
+
f"List must have at most {max_length} items, got: {len(value)}", field_name, value
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
if item_validator:
|
|
539
|
+
for i, item in enumerate(value):
|
|
540
|
+
try:
|
|
541
|
+
item_validator(item)
|
|
542
|
+
except ValidationError as e:
|
|
543
|
+
raise ValidationError(f"Invalid item at index {i}: {e.message}", field_name, value)
|
|
544
|
+
|
|
545
|
+
return value
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def validate_dict(
|
|
549
|
+
value: Any,
|
|
550
|
+
field_name: str = "value",
|
|
551
|
+
required_keys: Optional[List[str]] = None,
|
|
552
|
+
value_types: Optional[Dict[str, type]] = None,
|
|
553
|
+
) -> Dict[str, Any]:
|
|
554
|
+
"""Validate a dictionary value.
|
|
555
|
+
|
|
556
|
+
Args:
|
|
557
|
+
value: Value to validate
|
|
558
|
+
field_name: Name of the field for error messages
|
|
559
|
+
required_keys: List of required keys
|
|
560
|
+
value_types: Dictionary of keys to expected value types
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
Validated dictionary
|
|
564
|
+
|
|
565
|
+
Raises:
|
|
566
|
+
ValidationError: If value is invalid
|
|
567
|
+
"""
|
|
568
|
+
if not isinstance(value, dict):
|
|
569
|
+
raise ValidationError(f"Expected dict, got {type(value).__name__}", field_name, value)
|
|
570
|
+
|
|
571
|
+
if required_keys:
|
|
572
|
+
missing = [k for k in required_keys if k not in value]
|
|
573
|
+
if missing:
|
|
574
|
+
raise ValidationError(f"Missing required keys: {', '.join(missing)}", field_name, value)
|
|
575
|
+
|
|
576
|
+
if value_types:
|
|
577
|
+
for key, expected_type in value_types.items():
|
|
578
|
+
if key in value:
|
|
579
|
+
actual_value = value[key]
|
|
580
|
+
if actual_value is not None and not isinstance(actual_value, expected_type):
|
|
581
|
+
raise ValidationError(
|
|
582
|
+
f"Value for key '{key}' must be {expected_type.__name__}, "
|
|
583
|
+
f"got {type(actual_value).__name__}",
|
|
584
|
+
field_name,
|
|
585
|
+
value,
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
return value
|