claude-mpm 4.4.3__py3-none-any.whl → 4.4.4__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/agent_loader.py +3 -2
- claude_mpm/agents/agent_loader_integration.py +2 -1
- claude_mpm/agents/async_agent_loader.py +2 -2
- claude_mpm/agents/base_agent_loader.py +2 -2
- claude_mpm/agents/frontmatter_validator.py +1 -0
- claude_mpm/agents/system_agent_config.py +2 -1
- claude_mpm/cli/commands/doctor.py +44 -5
- claude_mpm/cli/commands/mpm_init.py +116 -62
- claude_mpm/cli/parsers/configure_parser.py +3 -1
- claude_mpm/cli/startup_logging.py +1 -3
- claude_mpm/config/agent_config.py +1 -1
- claude_mpm/config/paths.py +2 -1
- claude_mpm/core/agent_name_normalizer.py +1 -0
- claude_mpm/core/config.py +2 -1
- claude_mpm/core/config_aliases.py +2 -1
- claude_mpm/core/file_utils.py +0 -1
- claude_mpm/core/framework/__init__.py +6 -6
- claude_mpm/core/framework/formatters/__init__.py +2 -2
- claude_mpm/core/framework/formatters/capability_generator.py +19 -8
- claude_mpm/core/framework/formatters/content_formatter.py +8 -3
- claude_mpm/core/framework/formatters/context_generator.py +7 -3
- claude_mpm/core/framework/loaders/__init__.py +3 -3
- claude_mpm/core/framework/loaders/agent_loader.py +7 -3
- claude_mpm/core/framework/loaders/file_loader.py +16 -6
- claude_mpm/core/framework/loaders/instruction_loader.py +16 -6
- claude_mpm/core/framework/loaders/packaged_loader.py +36 -12
- claude_mpm/core/framework/processors/__init__.py +2 -2
- claude_mpm/core/framework/processors/memory_processor.py +14 -6
- claude_mpm/core/framework/processors/metadata_processor.py +5 -5
- claude_mpm/core/framework/processors/template_processor.py +12 -6
- claude_mpm/core/framework_loader.py +44 -20
- claude_mpm/core/log_manager.py +2 -1
- claude_mpm/core/tool_access_control.py +1 -0
- claude_mpm/core/unified_agent_registry.py +2 -1
- claude_mpm/core/unified_paths.py +1 -0
- claude_mpm/experimental/cli_enhancements.py +1 -0
- claude_mpm/hooks/base_hook.py +1 -0
- claude_mpm/hooks/instruction_reinforcement.py +1 -0
- claude_mpm/hooks/kuzu_memory_hook.py +20 -13
- claude_mpm/hooks/validation_hooks.py +1 -1
- claude_mpm/scripts/mpm_doctor.py +1 -0
- claude_mpm/services/agents/loading/agent_profile_loader.py +1 -1
- claude_mpm/services/agents/loading/base_agent_manager.py +1 -1
- claude_mpm/services/agents/loading/framework_agent_loader.py +1 -1
- claude_mpm/services/agents/management/agent_capabilities_generator.py +1 -0
- claude_mpm/services/agents/management/agent_management_service.py +1 -1
- claude_mpm/services/agents/memory/memory_categorization_service.py +0 -1
- claude_mpm/services/agents/memory/memory_file_service.py +6 -2
- claude_mpm/services/agents/memory/memory_format_service.py +0 -1
- claude_mpm/services/agents/registry/deployed_agent_discovery.py +1 -1
- claude_mpm/services/async_session_logger.py +1 -1
- claude_mpm/services/claude_session_logger.py +1 -0
- claude_mpm/services/core/path_resolver.py +1 -0
- claude_mpm/services/diagnostics/checks/__init__.py +2 -0
- claude_mpm/services/diagnostics/checks/installation_check.py +126 -25
- claude_mpm/services/diagnostics/checks/mcp_services_check.py +399 -0
- claude_mpm/services/diagnostics/diagnostic_runner.py +3 -0
- claude_mpm/services/diagnostics/doctor_reporter.py +259 -32
- claude_mpm/services/event_bus/direct_relay.py +2 -1
- claude_mpm/services/event_bus/event_bus.py +1 -0
- claude_mpm/services/event_bus/relay.py +3 -2
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +1 -1
- claude_mpm/services/infrastructure/daemon_manager.py +1 -1
- claude_mpm/services/mcp_config_manager.py +10 -10
- claude_mpm/services/mcp_gateway/core/process_pool.py +62 -23
- claude_mpm/services/mcp_gateway/tools/__init__.py +6 -5
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +3 -1
- claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +16 -31
- claude_mpm/services/memory/cache/simple_cache.py +1 -1
- claude_mpm/services/project/archive_manager.py +159 -96
- claude_mpm/services/project/documentation_manager.py +64 -45
- claude_mpm/services/project/enhanced_analyzer.py +132 -89
- claude_mpm/services/project/project_organizer.py +225 -131
- claude_mpm/services/response_tracker.py +1 -1
- claude_mpm/services/socketio/server/eventbus_integration.py +1 -1
- claude_mpm/services/unified/__init__.py +1 -1
- claude_mpm/services/unified/analyzer_strategies/__init__.py +3 -3
- claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +97 -53
- claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +81 -40
- claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +277 -178
- claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +196 -112
- claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +83 -49
- claude_mpm/services/unified/config_strategies/__init__.py +111 -126
- claude_mpm/services/unified/config_strategies/config_schema.py +157 -111
- claude_mpm/services/unified/config_strategies/context_strategy.py +91 -89
- claude_mpm/services/unified/config_strategies/error_handling_strategy.py +183 -173
- claude_mpm/services/unified/config_strategies/file_loader_strategy.py +160 -152
- claude_mpm/services/unified/config_strategies/unified_config_service.py +124 -112
- claude_mpm/services/unified/config_strategies/validation_strategy.py +298 -259
- claude_mpm/services/unified/deployment_strategies/__init__.py +7 -7
- claude_mpm/services/unified/deployment_strategies/base.py +24 -28
- claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +168 -88
- claude_mpm/services/unified/deployment_strategies/local.py +49 -34
- claude_mpm/services/unified/deployment_strategies/utils.py +39 -43
- claude_mpm/services/unified/deployment_strategies/vercel.py +30 -24
- claude_mpm/services/unified/interfaces.py +0 -26
- claude_mpm/services/unified/migration.py +17 -40
- claude_mpm/services/unified/strategies.py +9 -26
- claude_mpm/services/unified/unified_analyzer.py +48 -44
- claude_mpm/services/unified/unified_config.py +21 -19
- claude_mpm/services/unified/unified_deployment.py +21 -26
- claude_mpm/storage/state_storage.py +1 -0
- claude_mpm/utils/agent_dependency_loader.py +18 -6
- claude_mpm/utils/common.py +14 -12
- claude_mpm/utils/database_connector.py +15 -12
- claude_mpm/utils/error_handler.py +1 -0
- claude_mpm/utils/log_cleanup.py +1 -0
- claude_mpm/utils/path_operations.py +1 -0
- claude_mpm/utils/session_logging.py +1 -1
- claude_mpm/utils/subprocess_utils.py +1 -0
- claude_mpm/validation/agent_validator.py +1 -1
- {claude_mpm-4.4.3.dist-info → claude_mpm-4.4.4.dist-info}/METADATA +9 -3
- {claude_mpm-4.4.3.dist-info → claude_mpm-4.4.4.dist-info}/RECORD +118 -117
- {claude_mpm-4.4.3.dist-info → claude_mpm-4.4.4.dist-info}/WHEEL +0 -0
- {claude_mpm-4.4.3.dist-info → claude_mpm-4.4.4.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.4.3.dist-info → claude_mpm-4.4.4.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.4.3.dist-info → claude_mpm-4.4.4.dist-info}/top_level.txt +0 -0
@@ -3,23 +3,24 @@ Validation Strategy - Reduces 236 validation functions to 15 composable validato
|
|
3
3
|
Part of Phase 3 Configuration Consolidation
|
4
4
|
"""
|
5
5
|
|
6
|
+
import ipaddress
|
7
|
+
import re
|
8
|
+
import urllib.parse
|
6
9
|
from abc import ABC, abstractmethod
|
7
|
-
from typing import Any, Dict, List, Optional, Union, Callable, Pattern, Type
|
8
10
|
from dataclasses import dataclass, field
|
11
|
+
from datetime import datetime
|
9
12
|
from enum import Enum
|
10
|
-
import re
|
11
|
-
from datetime import datetime, date
|
12
13
|
from pathlib import Path
|
13
|
-
import
|
14
|
-
import urllib.parse
|
15
|
-
from collections.abc import Iterable
|
14
|
+
from typing import Any, Callable, Dict, List, Optional, Pattern, Union
|
16
15
|
|
17
16
|
from claude_mpm.core.logging_utils import get_logger
|
17
|
+
|
18
18
|
from .unified_config_service import IConfigStrategy
|
19
19
|
|
20
20
|
|
21
21
|
class ValidationType(Enum):
|
22
22
|
"""Types of validation operations"""
|
23
|
+
|
23
24
|
TYPE = "type"
|
24
25
|
REQUIRED = "required"
|
25
26
|
RANGE = "range"
|
@@ -40,6 +41,7 @@ class ValidationType(Enum):
|
|
40
41
|
@dataclass
|
41
42
|
class ValidationRule:
|
42
43
|
"""Single validation rule definition"""
|
44
|
+
|
43
45
|
type: ValidationType
|
44
46
|
params: Dict[str, Any] = field(default_factory=dict)
|
45
47
|
message: Optional[str] = None
|
@@ -50,6 +52,7 @@ class ValidationRule:
|
|
50
52
|
@dataclass
|
51
53
|
class ValidationResult:
|
52
54
|
"""Result of validation operation"""
|
55
|
+
|
53
56
|
valid: bool
|
54
57
|
errors: List[str] = field(default_factory=list)
|
55
58
|
warnings: List[str] = field(default_factory=list)
|
@@ -64,11 +67,14 @@ class BaseValidator(ABC):
|
|
64
67
|
self.logger = get_logger(self.__class__.__name__)
|
65
68
|
|
66
69
|
@abstractmethod
|
67
|
-
def validate(
|
70
|
+
def validate(
|
71
|
+
self, value: Any, rule: ValidationRule, context: Dict[str, Any]
|
72
|
+
) -> ValidationResult:
|
68
73
|
"""Perform validation"""
|
69
|
-
pass
|
70
74
|
|
71
|
-
def _create_result(
|
75
|
+
def _create_result(
|
76
|
+
self, valid: bool, message: str = None, severity: str = "error"
|
77
|
+
) -> ValidationResult:
|
72
78
|
"""Create validation result"""
|
73
79
|
result = ValidationResult(valid=valid)
|
74
80
|
|
@@ -87,26 +93,28 @@ class TypeValidator(BaseValidator):
|
|
87
93
|
"""Validates data types - replaces 45 type validation functions"""
|
88
94
|
|
89
95
|
TYPE_MAP = {
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
96
|
+
"string": str,
|
97
|
+
"str": str,
|
98
|
+
"integer": int,
|
99
|
+
"int": int,
|
100
|
+
"float": float,
|
101
|
+
"number": (int, float),
|
102
|
+
"boolean": bool,
|
103
|
+
"bool": bool,
|
104
|
+
"array": list,
|
105
|
+
"list": list,
|
106
|
+
"object": dict,
|
107
|
+
"dict": dict,
|
108
|
+
"null": type(None),
|
109
|
+
"none": type(None),
|
110
|
+
"any": object,
|
105
111
|
}
|
106
112
|
|
107
|
-
def validate(
|
113
|
+
def validate(
|
114
|
+
self, value: Any, rule: ValidationRule, context: Dict[str, Any]
|
115
|
+
) -> ValidationResult:
|
108
116
|
"""Validate value type"""
|
109
|
-
expected_type = rule.params.get(
|
117
|
+
expected_type = rule.params.get("type")
|
110
118
|
|
111
119
|
if not expected_type:
|
112
120
|
return self._create_result(True)
|
@@ -122,11 +130,11 @@ class TypeValidator(BaseValidator):
|
|
122
130
|
if isinstance(value, type_obj):
|
123
131
|
return self._create_result(True)
|
124
132
|
|
125
|
-
types_str =
|
133
|
+
types_str = ", ".join(str(t) for t in expected_type)
|
126
134
|
return self._create_result(
|
127
135
|
False,
|
128
136
|
f"Value must be one of types: {types_str}, got {type(value).__name__}",
|
129
|
-
rule.severity
|
137
|
+
rule.severity,
|
130
138
|
)
|
131
139
|
|
132
140
|
# Single type validation
|
@@ -134,7 +142,7 @@ class TypeValidator(BaseValidator):
|
|
134
142
|
return self._create_result(
|
135
143
|
False,
|
136
144
|
f"Expected type {expected_type}, got {type(value).__name__}",
|
137
|
-
rule.severity
|
145
|
+
rule.severity,
|
138
146
|
)
|
139
147
|
|
140
148
|
return self._create_result(True)
|
@@ -143,34 +151,33 @@ class TypeValidator(BaseValidator):
|
|
143
151
|
class RequiredValidator(BaseValidator):
|
144
152
|
"""Validates required fields - replaces 35 required validation functions"""
|
145
153
|
|
146
|
-
def validate(
|
154
|
+
def validate(
|
155
|
+
self, value: Any, rule: ValidationRule, context: Dict[str, Any]
|
156
|
+
) -> ValidationResult:
|
147
157
|
"""Validate required fields"""
|
148
|
-
required_fields = rule.params.get(
|
149
|
-
config = context.get(
|
158
|
+
required_fields = rule.params.get("fields", [])
|
159
|
+
config = context.get("config", {})
|
150
160
|
|
151
161
|
missing = []
|
152
162
|
for field in required_fields:
|
153
|
-
if
|
163
|
+
if "." in field:
|
154
164
|
# Nested field check
|
155
165
|
if not self._check_nested_field(config, field):
|
156
166
|
missing.append(field)
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
missing.append(field)
|
167
|
+
# Simple field check
|
168
|
+
elif field not in config or config[field] is None:
|
169
|
+
missing.append(field)
|
161
170
|
|
162
171
|
if missing:
|
163
172
|
return self._create_result(
|
164
|
-
False,
|
165
|
-
f"Required fields missing: {', '.join(missing)}",
|
166
|
-
rule.severity
|
173
|
+
False, f"Required fields missing: {', '.join(missing)}", rule.severity
|
167
174
|
)
|
168
175
|
|
169
176
|
return self._create_result(True)
|
170
177
|
|
171
178
|
def _check_nested_field(self, obj: Dict, path: str) -> bool:
|
172
179
|
"""Check if nested field exists"""
|
173
|
-
parts = path.split(
|
180
|
+
parts = path.split(".")
|
174
181
|
current = obj
|
175
182
|
|
176
183
|
for part in parts:
|
@@ -184,15 +191,17 @@ class RequiredValidator(BaseValidator):
|
|
184
191
|
class RangeValidator(BaseValidator):
|
185
192
|
"""Validates numeric ranges - replaces 28 range validation functions"""
|
186
193
|
|
187
|
-
def validate(
|
194
|
+
def validate(
|
195
|
+
self, value: Any, rule: ValidationRule, context: Dict[str, Any]
|
196
|
+
) -> ValidationResult:
|
188
197
|
"""Validate numeric range"""
|
189
198
|
if not isinstance(value, (int, float)):
|
190
199
|
return self._create_result(True) # Skip non-numeric values
|
191
200
|
|
192
|
-
min_val = rule.params.get(
|
193
|
-
max_val = rule.params.get(
|
194
|
-
exclusive_min = rule.params.get(
|
195
|
-
exclusive_max = rule.params.get(
|
201
|
+
min_val = rule.params.get("min")
|
202
|
+
max_val = rule.params.get("max")
|
203
|
+
exclusive_min = rule.params.get("exclusive_min", False)
|
204
|
+
exclusive_max = rule.params.get("exclusive_max", False)
|
196
205
|
|
197
206
|
# Check minimum
|
198
207
|
if min_val is not None:
|
@@ -200,28 +209,22 @@ class RangeValidator(BaseValidator):
|
|
200
209
|
return self._create_result(
|
201
210
|
False,
|
202
211
|
f"Value {value} must be greater than {min_val}",
|
203
|
-
rule.severity
|
212
|
+
rule.severity,
|
204
213
|
)
|
205
|
-
|
214
|
+
if not exclusive_min and value < min_val:
|
206
215
|
return self._create_result(
|
207
|
-
False,
|
208
|
-
f"Value {value} must be at least {min_val}",
|
209
|
-
rule.severity
|
216
|
+
False, f"Value {value} must be at least {min_val}", rule.severity
|
210
217
|
)
|
211
218
|
|
212
219
|
# Check maximum
|
213
220
|
if max_val is not None:
|
214
221
|
if exclusive_max and value >= max_val:
|
215
222
|
return self._create_result(
|
216
|
-
False,
|
217
|
-
f"Value {value} must be less than {max_val}",
|
218
|
-
rule.severity
|
223
|
+
False, f"Value {value} must be less than {max_val}", rule.severity
|
219
224
|
)
|
220
|
-
|
225
|
+
if not exclusive_max and value > max_val:
|
221
226
|
return self._create_result(
|
222
|
-
False,
|
223
|
-
f"Value {value} must be at most {max_val}",
|
224
|
-
rule.severity
|
227
|
+
False, f"Value {value} must be at most {max_val}", rule.severity
|
225
228
|
)
|
226
229
|
|
227
230
|
return self._create_result(True)
|
@@ -230,22 +233,24 @@ class RangeValidator(BaseValidator):
|
|
230
233
|
class LengthValidator(BaseValidator):
|
231
234
|
"""Validates string/array lengths - replaces 22 length validation functions"""
|
232
235
|
|
233
|
-
def validate(
|
236
|
+
def validate(
|
237
|
+
self, value: Any, rule: ValidationRule, context: Dict[str, Any]
|
238
|
+
) -> ValidationResult:
|
234
239
|
"""Validate length constraints"""
|
235
|
-
if not hasattr(value,
|
240
|
+
if not hasattr(value, "__len__"):
|
236
241
|
return self._create_result(True) # Skip non-sized values
|
237
242
|
|
238
243
|
length = len(value)
|
239
|
-
min_length = rule.params.get(
|
240
|
-
max_length = rule.params.get(
|
241
|
-
exact_length = rule.params.get(
|
244
|
+
min_length = rule.params.get("min")
|
245
|
+
max_length = rule.params.get("max")
|
246
|
+
exact_length = rule.params.get("exact")
|
242
247
|
|
243
248
|
# Check exact length
|
244
249
|
if exact_length is not None and length != exact_length:
|
245
250
|
return self._create_result(
|
246
251
|
False,
|
247
252
|
f"Length must be exactly {exact_length}, got {length}",
|
248
|
-
rule.severity
|
253
|
+
rule.severity,
|
249
254
|
)
|
250
255
|
|
251
256
|
# Check minimum length
|
@@ -253,7 +258,7 @@ class LengthValidator(BaseValidator):
|
|
253
258
|
return self._create_result(
|
254
259
|
False,
|
255
260
|
f"Length must be at least {min_length}, got {length}",
|
256
|
-
rule.severity
|
261
|
+
rule.severity,
|
257
262
|
)
|
258
263
|
|
259
264
|
# Check maximum length
|
@@ -261,7 +266,7 @@ class LengthValidator(BaseValidator):
|
|
261
266
|
return self._create_result(
|
262
267
|
False,
|
263
268
|
f"Length must be at most {max_length}, got {length}",
|
264
|
-
rule.severity
|
269
|
+
rule.severity,
|
265
270
|
)
|
266
271
|
|
267
272
|
return self._create_result(True)
|
@@ -274,12 +279,14 @@ class PatternValidator(BaseValidator):
|
|
274
279
|
super().__init__()
|
275
280
|
self._compiled_patterns: Dict[str, Pattern] = {}
|
276
281
|
|
277
|
-
def validate(
|
282
|
+
def validate(
|
283
|
+
self, value: Any, rule: ValidationRule, context: Dict[str, Any]
|
284
|
+
) -> ValidationResult:
|
278
285
|
"""Validate against regex pattern"""
|
279
286
|
if not isinstance(value, str):
|
280
287
|
return self._create_result(True) # Skip non-string values
|
281
288
|
|
282
|
-
pattern = rule.params.get(
|
289
|
+
pattern = rule.params.get("pattern")
|
283
290
|
if not pattern:
|
284
291
|
return self._create_result(True)
|
285
292
|
|
@@ -294,7 +301,9 @@ class PatternValidator(BaseValidator):
|
|
294
301
|
|
295
302
|
# Check match
|
296
303
|
if not regex.match(value):
|
297
|
-
message = rule.params.get(
|
304
|
+
message = rule.params.get(
|
305
|
+
"message", f"Value does not match pattern: {pattern}"
|
306
|
+
)
|
298
307
|
return self._create_result(False, message, rule.severity)
|
299
308
|
|
300
309
|
return self._create_result(True)
|
@@ -303,33 +312,36 @@ class PatternValidator(BaseValidator):
|
|
303
312
|
class EnumValidator(BaseValidator):
|
304
313
|
"""Validates enum values - replaces 18 enum validation functions"""
|
305
314
|
|
306
|
-
def validate(
|
315
|
+
def validate(
|
316
|
+
self, value: Any, rule: ValidationRule, context: Dict[str, Any]
|
317
|
+
) -> ValidationResult:
|
307
318
|
"""Validate enum membership"""
|
308
|
-
allowed_values = rule.params.get(
|
319
|
+
allowed_values = rule.params.get("values", [])
|
309
320
|
|
310
321
|
if not allowed_values:
|
311
322
|
return self._create_result(True)
|
312
323
|
|
313
324
|
# Handle case sensitivity
|
314
|
-
case_sensitive = rule.params.get(
|
325
|
+
case_sensitive = rule.params.get("case_sensitive", True)
|
315
326
|
|
316
327
|
if not case_sensitive and isinstance(value, str):
|
317
328
|
value_lower = value.lower()
|
318
|
-
allowed_lower = [
|
329
|
+
allowed_lower = [
|
330
|
+
v.lower() if isinstance(v, str) else v for v in allowed_values
|
331
|
+
]
|
319
332
|
|
320
333
|
if value_lower not in allowed_lower:
|
321
334
|
return self._create_result(
|
322
335
|
False,
|
323
336
|
f"Value '{value}' not in allowed values: {allowed_values}",
|
324
|
-
rule.severity
|
325
|
-
)
|
326
|
-
else:
|
327
|
-
if value not in allowed_values:
|
328
|
-
return self._create_result(
|
329
|
-
False,
|
330
|
-
f"Value '{value}' not in allowed values: {allowed_values}",
|
331
|
-
rule.severity
|
337
|
+
rule.severity,
|
332
338
|
)
|
339
|
+
elif value not in allowed_values:
|
340
|
+
return self._create_result(
|
341
|
+
False,
|
342
|
+
f"Value '{value}' not in allowed values: {allowed_values}",
|
343
|
+
rule.severity,
|
344
|
+
)
|
333
345
|
|
334
346
|
return self._create_result(True)
|
335
347
|
|
@@ -338,50 +350,51 @@ class FormatValidator(BaseValidator):
|
|
338
350
|
"""Validates common formats - replaces 24 format validation functions"""
|
339
351
|
|
340
352
|
FORMAT_VALIDATORS = {
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
353
|
+
"email": lambda v: "@" in v and "." in v.split("@")[1],
|
354
|
+
"url": lambda v: FormatValidator._validate_url(v),
|
355
|
+
"uri": lambda v: FormatValidator._validate_uri(v),
|
356
|
+
"uuid": lambda v: FormatValidator._validate_uuid(v),
|
357
|
+
"ipv4": lambda v: FormatValidator._validate_ipv4(v),
|
358
|
+
"ipv6": lambda v: FormatValidator._validate_ipv6(v),
|
359
|
+
"ip": lambda v: FormatValidator._validate_ip(v),
|
360
|
+
"hostname": lambda v: FormatValidator._validate_hostname(v),
|
361
|
+
"date": lambda v: FormatValidator._validate_date(v),
|
362
|
+
"time": lambda v: FormatValidator._validate_time(v),
|
363
|
+
"datetime": lambda v: FormatValidator._validate_datetime(v),
|
364
|
+
"json": lambda v: FormatValidator._validate_json(v),
|
365
|
+
"base64": lambda v: FormatValidator._validate_base64(v),
|
366
|
+
"path": lambda v: FormatValidator._validate_path(v),
|
367
|
+
"semver": lambda v: FormatValidator._validate_semver(v),
|
356
368
|
}
|
357
369
|
|
358
|
-
def validate(
|
370
|
+
def validate(
|
371
|
+
self, value: Any, rule: ValidationRule, context: Dict[str, Any]
|
372
|
+
) -> ValidationResult:
|
359
373
|
"""Validate format"""
|
360
374
|
if not isinstance(value, str):
|
361
375
|
return self._create_result(True)
|
362
376
|
|
363
|
-
format_type = rule.params.get(
|
377
|
+
format_type = rule.params.get("format")
|
364
378
|
if not format_type:
|
365
379
|
return self._create_result(True)
|
366
380
|
|
367
381
|
validator = self.FORMAT_VALIDATORS.get(format_type)
|
368
382
|
if not validator:
|
369
|
-
return self._create_result(
|
383
|
+
return self._create_result(
|
384
|
+
False, f"Unknown format: {format_type}", "warning"
|
385
|
+
)
|
370
386
|
|
371
387
|
try:
|
372
388
|
if validator(value):
|
373
389
|
return self._create_result(True)
|
374
|
-
else:
|
375
|
-
return self._create_result(
|
376
|
-
False,
|
377
|
-
f"Value '{value}' is not a valid {format_type}",
|
378
|
-
rule.severity
|
379
|
-
)
|
380
|
-
except Exception as e:
|
381
390
|
return self._create_result(
|
382
391
|
False,
|
383
|
-
f"
|
384
|
-
rule.severity
|
392
|
+
f"Value '{value}' is not a valid {format_type}",
|
393
|
+
rule.severity,
|
394
|
+
)
|
395
|
+
except Exception as e:
|
396
|
+
return self._create_result(
|
397
|
+
False, f"Format validation failed: {e}", rule.severity
|
385
398
|
)
|
386
399
|
|
387
400
|
@staticmethod
|
@@ -403,6 +416,7 @@ class FormatValidator(BaseValidator):
|
|
403
416
|
@staticmethod
|
404
417
|
def _validate_uuid(value: str) -> bool:
|
405
418
|
import uuid
|
419
|
+
|
406
420
|
try:
|
407
421
|
uuid.UUID(value)
|
408
422
|
return True
|
@@ -436,16 +450,16 @@ class FormatValidator(BaseValidator):
|
|
436
450
|
@staticmethod
|
437
451
|
def _validate_hostname(value: str) -> bool:
|
438
452
|
pattern = re.compile(
|
439
|
-
r
|
440
|
-
r
|
441
|
-
r
|
453
|
+
r"^(?=.{1,253}$)(?!-)(?!.*--)"
|
454
|
+
r"[a-zA-Z0-9-]{1,63}"
|
455
|
+
r"(?:\.[a-zA-Z0-9-]{1,63})*$"
|
442
456
|
)
|
443
457
|
return bool(pattern.match(value))
|
444
458
|
|
445
459
|
@staticmethod
|
446
460
|
def _validate_date(value: str) -> bool:
|
447
461
|
try:
|
448
|
-
datetime.strptime(value,
|
462
|
+
datetime.strptime(value, "%Y-%m-%d")
|
449
463
|
return True
|
450
464
|
except:
|
451
465
|
return False
|
@@ -453,11 +467,11 @@ class FormatValidator(BaseValidator):
|
|
453
467
|
@staticmethod
|
454
468
|
def _validate_time(value: str) -> bool:
|
455
469
|
try:
|
456
|
-
datetime.strptime(value,
|
470
|
+
datetime.strptime(value, "%H:%M:%S")
|
457
471
|
return True
|
458
472
|
except:
|
459
473
|
try:
|
460
|
-
datetime.strptime(value,
|
474
|
+
datetime.strptime(value, "%H:%M")
|
461
475
|
return True
|
462
476
|
except:
|
463
477
|
return False
|
@@ -465,10 +479,10 @@ class FormatValidator(BaseValidator):
|
|
465
479
|
@staticmethod
|
466
480
|
def _validate_datetime(value: str) -> bool:
|
467
481
|
formats = [
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
482
|
+
"%Y-%m-%d %H:%M:%S",
|
483
|
+
"%Y-%m-%dT%H:%M:%S",
|
484
|
+
"%Y-%m-%dT%H:%M:%SZ",
|
485
|
+
"%Y-%m-%dT%H:%M:%S%z",
|
472
486
|
]
|
473
487
|
for fmt in formats:
|
474
488
|
try:
|
@@ -481,6 +495,7 @@ class FormatValidator(BaseValidator):
|
|
481
495
|
@staticmethod
|
482
496
|
def _validate_json(value: str) -> bool:
|
483
497
|
import json
|
498
|
+
|
484
499
|
try:
|
485
500
|
json.loads(value)
|
486
501
|
return True
|
@@ -490,6 +505,7 @@ class FormatValidator(BaseValidator):
|
|
490
505
|
@staticmethod
|
491
506
|
def _validate_base64(value: str) -> bool:
|
492
507
|
import base64
|
508
|
+
|
493
509
|
try:
|
494
510
|
base64.b64decode(value, validate=True)
|
495
511
|
return True
|
@@ -507,10 +523,10 @@ class FormatValidator(BaseValidator):
|
|
507
523
|
@staticmethod
|
508
524
|
def _validate_semver(value: str) -> bool:
|
509
525
|
pattern = re.compile(
|
510
|
-
r
|
511
|
-
r
|
512
|
-
r
|
513
|
-
r
|
526
|
+
r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)"
|
527
|
+
r"(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)"
|
528
|
+
r"(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?"
|
529
|
+
r"(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
|
514
530
|
)
|
515
531
|
return bool(pattern.match(value))
|
516
532
|
|
@@ -518,10 +534,12 @@ class FormatValidator(BaseValidator):
|
|
518
534
|
class DependencyValidator(BaseValidator):
|
519
535
|
"""Validates field dependencies - replaces 16 dependency validation functions"""
|
520
536
|
|
521
|
-
def validate(
|
537
|
+
def validate(
|
538
|
+
self, value: Any, rule: ValidationRule, context: Dict[str, Any]
|
539
|
+
) -> ValidationResult:
|
522
540
|
"""Validate field dependencies"""
|
523
|
-
config = context.get(
|
524
|
-
dependencies = rule.params.get(
|
541
|
+
config = context.get("config", {})
|
542
|
+
dependencies = rule.params.get("dependencies", {})
|
525
543
|
|
526
544
|
errors = []
|
527
545
|
|
@@ -552,7 +570,9 @@ class DependencyValidator(BaseValidator):
|
|
552
570
|
class UniqueValidator(BaseValidator):
|
553
571
|
"""Validates unique values - replaces 12 unique validation functions"""
|
554
572
|
|
555
|
-
def validate(
|
573
|
+
def validate(
|
574
|
+
self, value: Any, rule: ValidationRule, context: Dict[str, Any]
|
575
|
+
) -> ValidationResult:
|
556
576
|
"""Validate unique values in collections"""
|
557
577
|
if not isinstance(value, (list, tuple)):
|
558
578
|
return self._create_result(True)
|
@@ -573,9 +593,7 @@ class UniqueValidator(BaseValidator):
|
|
573
593
|
|
574
594
|
if duplicates:
|
575
595
|
return self._create_result(
|
576
|
-
False,
|
577
|
-
f"Duplicate values found: {duplicates}",
|
578
|
-
rule.severity
|
596
|
+
False, f"Duplicate values found: {duplicates}", rule.severity
|
579
597
|
)
|
580
598
|
|
581
599
|
return self._create_result(True)
|
@@ -584,9 +602,11 @@ class UniqueValidator(BaseValidator):
|
|
584
602
|
class CustomValidator(BaseValidator):
|
585
603
|
"""Executes custom validation functions - replaces 20 custom validation functions"""
|
586
604
|
|
587
|
-
def validate(
|
605
|
+
def validate(
|
606
|
+
self, value: Any, rule: ValidationRule, context: Dict[str, Any]
|
607
|
+
) -> ValidationResult:
|
588
608
|
"""Execute custom validation function"""
|
589
|
-
validator_func = rule.params.get(
|
609
|
+
validator_func = rule.params.get("function")
|
590
610
|
|
591
611
|
if not validator_func or not callable(validator_func):
|
592
612
|
return self._create_result(True)
|
@@ -598,44 +618,41 @@ class CustomValidator(BaseValidator):
|
|
598
618
|
# Handle different return types
|
599
619
|
if isinstance(result, bool):
|
600
620
|
if not result:
|
601
|
-
message = rule.params.get(
|
621
|
+
message = rule.params.get("message", "Custom validation failed")
|
602
622
|
return self._create_result(False, message, rule.severity)
|
603
623
|
return self._create_result(True)
|
604
624
|
|
605
|
-
|
625
|
+
if isinstance(result, str):
|
606
626
|
# String means validation failed with that message
|
607
627
|
return self._create_result(False, result, rule.severity)
|
608
628
|
|
609
|
-
|
629
|
+
if isinstance(result, tuple):
|
610
630
|
# (valid, message) tuple
|
611
631
|
valid, message = result
|
612
632
|
if not valid:
|
613
633
|
return self._create_result(False, message, rule.severity)
|
614
634
|
return self._create_result(True)
|
615
635
|
|
616
|
-
|
636
|
+
if isinstance(result, ValidationResult):
|
617
637
|
return result
|
618
638
|
|
619
|
-
|
620
|
-
|
621
|
-
return self._create_result(bool(result))
|
639
|
+
# Unknown return type, assume success if truthy
|
640
|
+
return self._create_result(bool(result))
|
622
641
|
|
623
642
|
except Exception as e:
|
624
|
-
return self._create_result(
|
625
|
-
False,
|
626
|
-
f"Custom validation error: {e}",
|
627
|
-
"error"
|
628
|
-
)
|
643
|
+
return self._create_result(False, f"Custom validation error: {e}", "error")
|
629
644
|
|
630
645
|
|
631
646
|
class ConditionalValidator(BaseValidator):
|
632
647
|
"""Validates based on conditions - replaces 15 conditional validation functions"""
|
633
648
|
|
634
|
-
def validate(
|
649
|
+
def validate(
|
650
|
+
self, value: Any, rule: ValidationRule, context: Dict[str, Any]
|
651
|
+
) -> ValidationResult:
|
635
652
|
"""Perform conditional validation"""
|
636
|
-
condition = rule.params.get(
|
637
|
-
then_rule = rule.params.get(
|
638
|
-
else_rule = rule.params.get(
|
653
|
+
condition = rule.params.get("if")
|
654
|
+
then_rule = rule.params.get("then")
|
655
|
+
else_rule = rule.params.get("else")
|
639
656
|
|
640
657
|
if not condition:
|
641
658
|
return self._create_result(True)
|
@@ -646,23 +663,25 @@ class ConditionalValidator(BaseValidator):
|
|
646
663
|
# Apply appropriate rule
|
647
664
|
if condition_met and then_rule:
|
648
665
|
return self._apply_rule(then_rule, value, context)
|
649
|
-
|
666
|
+
if not condition_met and else_rule:
|
650
667
|
return self._apply_rule(else_rule, value, context)
|
651
668
|
|
652
669
|
return self._create_result(True)
|
653
670
|
|
654
|
-
def _evaluate_condition(
|
671
|
+
def _evaluate_condition(
|
672
|
+
self, condition: Any, value: Any, context: Dict[str, Any]
|
673
|
+
) -> bool:
|
655
674
|
"""Evaluate condition"""
|
656
675
|
if callable(condition):
|
657
676
|
return condition(value, context)
|
658
677
|
|
659
678
|
if isinstance(condition, dict):
|
660
679
|
# Field comparison condition
|
661
|
-
field = condition.get(
|
662
|
-
operator = condition.get(
|
663
|
-
expected = condition.get(
|
680
|
+
field = condition.get("field")
|
681
|
+
operator = condition.get("operator", "==")
|
682
|
+
expected = condition.get("value")
|
664
683
|
|
665
|
-
config = context.get(
|
684
|
+
config = context.get("config", {})
|
666
685
|
actual = config.get(field)
|
667
686
|
|
668
687
|
return self._compare_values(actual, operator, expected)
|
@@ -672,16 +691,16 @@ class ConditionalValidator(BaseValidator):
|
|
672
691
|
def _compare_values(self, actual: Any, operator: str, expected: Any) -> bool:
|
673
692
|
"""Compare values with operator"""
|
674
693
|
operators = {
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
694
|
+
"==": lambda a, e: a == e,
|
695
|
+
"!=": lambda a, e: a != e,
|
696
|
+
"<": lambda a, e: a < e,
|
697
|
+
"<=": lambda a, e: a <= e,
|
698
|
+
">": lambda a, e: a > e,
|
699
|
+
">=": lambda a, e: a >= e,
|
700
|
+
"in": lambda a, e: a in e,
|
701
|
+
"not_in": lambda a, e: a not in e,
|
702
|
+
"contains": lambda a, e: e in a,
|
703
|
+
"matches": lambda a, e: re.match(e, str(a)) is not None,
|
685
704
|
}
|
686
705
|
|
687
706
|
comparator = operators.get(operator)
|
@@ -693,14 +712,16 @@ class ConditionalValidator(BaseValidator):
|
|
693
712
|
except:
|
694
713
|
return False
|
695
714
|
|
696
|
-
def _apply_rule(
|
715
|
+
def _apply_rule(
|
716
|
+
self, rule_def: Dict, value: Any, context: Dict[str, Any]
|
717
|
+
) -> ValidationResult:
|
697
718
|
"""Apply validation rule"""
|
698
719
|
# Create and execute rule
|
699
720
|
rule = ValidationRule(
|
700
|
-
type=ValidationType[rule_def.get(
|
701
|
-
params=rule_def.get(
|
702
|
-
message=rule_def.get(
|
703
|
-
severity=rule_def.get(
|
721
|
+
type=ValidationType[rule_def.get("type", "CUSTOM").upper()],
|
722
|
+
params=rule_def.get("params", {}),
|
723
|
+
message=rule_def.get("message"),
|
724
|
+
severity=rule_def.get("severity", "error"),
|
704
725
|
)
|
705
726
|
|
706
727
|
# Find appropriate validator
|
@@ -714,14 +735,18 @@ class ConditionalValidator(BaseValidator):
|
|
714
735
|
class RecursiveValidator(BaseValidator):
|
715
736
|
"""Validates nested structures recursively - replaces 10 recursive validation functions"""
|
716
737
|
|
717
|
-
def validate(
|
738
|
+
def validate(
|
739
|
+
self, value: Any, rule: ValidationRule, context: Dict[str, Any]
|
740
|
+
) -> ValidationResult:
|
718
741
|
"""Recursively validate nested structures"""
|
719
|
-
schema = rule.params.get(
|
720
|
-
max_depth = rule.params.get(
|
721
|
-
current_depth = context.get(
|
742
|
+
schema = rule.params.get("schema", {})
|
743
|
+
max_depth = rule.params.get("max_depth", 10)
|
744
|
+
current_depth = context.get("_depth", 0)
|
722
745
|
|
723
746
|
if current_depth >= max_depth:
|
724
|
-
return self._create_result(
|
747
|
+
return self._create_result(
|
748
|
+
False, f"Maximum recursion depth {max_depth} exceeded", "error"
|
749
|
+
)
|
725
750
|
|
726
751
|
result = ValidationResult(valid=True)
|
727
752
|
|
@@ -729,33 +754,36 @@ class RecursiveValidator(BaseValidator):
|
|
729
754
|
result = self._validate_dict(value, schema, context, current_depth)
|
730
755
|
elif isinstance(value, list):
|
731
756
|
result = self._validate_list(value, schema, context, current_depth)
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
type_validator = TypeValidator()
|
740
|
-
result = type_validator.validate(value, type_rule, context)
|
757
|
+
# Validate single value
|
758
|
+
elif "type" in schema:
|
759
|
+
type_rule = ValidationRule(
|
760
|
+
type=ValidationType.TYPE, params={"type": schema["type"]}
|
761
|
+
)
|
762
|
+
type_validator = TypeValidator()
|
763
|
+
result = type_validator.validate(value, type_rule, context)
|
741
764
|
|
742
765
|
return result
|
743
766
|
|
744
|
-
def _validate_dict(
|
767
|
+
def _validate_dict(
|
768
|
+
self, value: Dict, schema: Dict, context: Dict, depth: int
|
769
|
+
) -> ValidationResult:
|
745
770
|
"""Validate dictionary recursively"""
|
746
771
|
result = ValidationResult(valid=True)
|
747
|
-
properties = schema.get(
|
772
|
+
properties = schema.get("properties", {})
|
748
773
|
|
749
774
|
for key, prop_schema in properties.items():
|
750
775
|
if key in value:
|
751
776
|
# Create context for nested validation
|
752
777
|
nested_context = context.copy()
|
753
|
-
nested_context[
|
778
|
+
nested_context["_depth"] = depth + 1
|
754
779
|
|
755
780
|
# Recursively validate
|
756
781
|
nested_rule = ValidationRule(
|
757
782
|
type=ValidationType.RECURSIVE,
|
758
|
-
params={
|
783
|
+
params={
|
784
|
+
"schema": prop_schema,
|
785
|
+
"max_depth": context.get("max_depth", 10),
|
786
|
+
},
|
759
787
|
)
|
760
788
|
|
761
789
|
nested_result = self.validate(value[key], nested_rule, nested_context)
|
@@ -763,24 +791,31 @@ class RecursiveValidator(BaseValidator):
|
|
763
791
|
if not nested_result.valid:
|
764
792
|
result.valid = False
|
765
793
|
result.errors.extend([f"{key}.{e}" for e in nested_result.errors])
|
766
|
-
result.warnings.extend(
|
794
|
+
result.warnings.extend(
|
795
|
+
[f"{key}.{w}" for w in nested_result.warnings]
|
796
|
+
)
|
767
797
|
|
768
798
|
return result
|
769
799
|
|
770
|
-
def _validate_list(
|
800
|
+
def _validate_list(
|
801
|
+
self, value: List, schema: Dict, context: Dict, depth: int
|
802
|
+
) -> ValidationResult:
|
771
803
|
"""Validate list recursively"""
|
772
804
|
result = ValidationResult(valid=True)
|
773
|
-
items_schema = schema.get(
|
805
|
+
items_schema = schema.get("items", {})
|
774
806
|
|
775
807
|
for i, item in enumerate(value):
|
776
808
|
# Create context for nested validation
|
777
809
|
nested_context = context.copy()
|
778
|
-
nested_context[
|
810
|
+
nested_context["_depth"] = depth + 1
|
779
811
|
|
780
812
|
# Recursively validate
|
781
813
|
nested_rule = ValidationRule(
|
782
814
|
type=ValidationType.RECURSIVE,
|
783
|
-
params={
|
815
|
+
params={
|
816
|
+
"schema": items_schema,
|
817
|
+
"max_depth": context.get("max_depth", 10),
|
818
|
+
},
|
784
819
|
)
|
785
820
|
|
786
821
|
nested_result = self.validate(item, nested_rule, nested_context)
|
@@ -796,16 +831,18 @@ class RecursiveValidator(BaseValidator):
|
|
796
831
|
class CrossFieldValidator(BaseValidator):
|
797
832
|
"""Validates cross-field constraints - replaces 8 cross-field validation functions"""
|
798
833
|
|
799
|
-
def validate(
|
834
|
+
def validate(
|
835
|
+
self, value: Any, rule: ValidationRule, context: Dict[str, Any]
|
836
|
+
) -> ValidationResult:
|
800
837
|
"""Validate cross-field constraints"""
|
801
|
-
config = context.get(
|
802
|
-
constraints = rule.params.get(
|
838
|
+
config = context.get("config", {})
|
839
|
+
constraints = rule.params.get("constraints", [])
|
803
840
|
|
804
841
|
result = ValidationResult(valid=True)
|
805
842
|
|
806
843
|
for constraint in constraints:
|
807
844
|
if not self._evaluate_constraint(config, constraint):
|
808
|
-
message = constraint.get(
|
845
|
+
message = constraint.get("message", "Cross-field constraint failed")
|
809
846
|
if rule.severity == "error":
|
810
847
|
result.errors.append(message)
|
811
848
|
elif rule.severity == "warning":
|
@@ -818,41 +855,41 @@ class CrossFieldValidator(BaseValidator):
|
|
818
855
|
|
819
856
|
def _evaluate_constraint(self, config: Dict, constraint: Dict) -> bool:
|
820
857
|
"""Evaluate a cross-field constraint"""
|
821
|
-
constraint_type = constraint.get(
|
858
|
+
constraint_type = constraint.get("type")
|
822
859
|
|
823
|
-
if constraint_type ==
|
860
|
+
if constraint_type == "mutual_exclusive":
|
824
861
|
# Only one of the fields should be present
|
825
|
-
fields = constraint.get(
|
862
|
+
fields = constraint.get("fields", [])
|
826
863
|
present = [f for f in fields if f in config and config[f] is not None]
|
827
864
|
return len(present) <= 1
|
828
865
|
|
829
|
-
|
866
|
+
if constraint_type == "mutual_required":
|
830
867
|
# All fields must be present together
|
831
|
-
fields = constraint.get(
|
868
|
+
fields = constraint.get("fields", [])
|
832
869
|
present = [f for f in fields if f in config and config[f] is not None]
|
833
870
|
return len(present) == 0 or len(present) == len(fields)
|
834
871
|
|
835
|
-
|
872
|
+
if constraint_type == "sum":
|
836
873
|
# Sum of fields must match condition
|
837
|
-
fields = constraint.get(
|
838
|
-
operator = constraint.get(
|
839
|
-
target = constraint.get(
|
874
|
+
fields = constraint.get("fields", [])
|
875
|
+
operator = constraint.get("operator", "==")
|
876
|
+
target = constraint.get("value", 0)
|
840
877
|
|
841
878
|
total = sum(config.get(f, 0) for f in fields)
|
842
879
|
return self._compare_values(total, operator, target)
|
843
880
|
|
844
|
-
|
881
|
+
if constraint_type == "comparison":
|
845
882
|
# Compare two fields
|
846
|
-
field1 = constraint.get(
|
847
|
-
field2 = constraint.get(
|
848
|
-
operator = constraint.get(
|
883
|
+
field1 = constraint.get("field1")
|
884
|
+
field2 = constraint.get("field2")
|
885
|
+
operator = constraint.get("operator", "==")
|
849
886
|
|
850
887
|
if field1 in config and field2 in config:
|
851
888
|
return self._compare_values(config[field1], operator, config[field2])
|
852
889
|
|
853
|
-
elif constraint_type ==
|
890
|
+
elif constraint_type == "custom":
|
854
891
|
# Custom constraint function
|
855
|
-
func = constraint.get(
|
892
|
+
func = constraint.get("function")
|
856
893
|
if callable(func):
|
857
894
|
return func(config)
|
858
895
|
|
@@ -861,12 +898,12 @@ class CrossFieldValidator(BaseValidator):
|
|
861
898
|
def _compare_values(self, val1: Any, operator: str, val2: Any) -> bool:
|
862
899
|
"""Compare two values with operator"""
|
863
900
|
operators = {
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
901
|
+
"==": lambda a, b: a == b,
|
902
|
+
"!=": lambda a, b: a != b,
|
903
|
+
"<": lambda a, b: a < b,
|
904
|
+
"<=": lambda a, b: a <= b,
|
905
|
+
">": lambda a, b: a > b,
|
906
|
+
">=": lambda a, b: a >= b,
|
870
907
|
}
|
871
908
|
|
872
909
|
comparator = operators.get(operator)
|
@@ -882,10 +919,12 @@ class CrossFieldValidator(BaseValidator):
|
|
882
919
|
class CompositeValidator(BaseValidator):
|
883
920
|
"""Composes multiple validators - replaces 6 composite validation functions"""
|
884
921
|
|
885
|
-
def validate(
|
922
|
+
def validate(
|
923
|
+
self, value: Any, rule: ValidationRule, context: Dict[str, Any]
|
924
|
+
) -> ValidationResult:
|
886
925
|
"""Apply multiple validators in sequence"""
|
887
|
-
validators = rule.params.get(
|
888
|
-
operator = rule.params.get(
|
926
|
+
validators = rule.params.get("validators", [])
|
927
|
+
operator = rule.params.get("operator", "AND") # AND, OR
|
889
928
|
|
890
929
|
results = []
|
891
930
|
combined_result = ValidationResult(valid=True)
|
@@ -896,10 +935,10 @@ class CompositeValidator(BaseValidator):
|
|
896
935
|
for validator_def in validators:
|
897
936
|
# Create rule from definition
|
898
937
|
val_rule = ValidationRule(
|
899
|
-
type=ValidationType[validator_def.get(
|
900
|
-
params=validator_def.get(
|
901
|
-
message=validator_def.get(
|
902
|
-
severity=validator_def.get(
|
938
|
+
type=ValidationType[validator_def.get("type", "CUSTOM").upper()],
|
939
|
+
params=validator_def.get("params", {}),
|
940
|
+
message=validator_def.get("message"),
|
941
|
+
severity=validator_def.get("severity", rule.severity),
|
903
942
|
)
|
904
943
|
|
905
944
|
# Get validator and execute
|
@@ -914,9 +953,9 @@ class CompositeValidator(BaseValidator):
|
|
914
953
|
combined_result.info.extend(result.info)
|
915
954
|
|
916
955
|
# Apply operator logic
|
917
|
-
if operator ==
|
956
|
+
if operator == "AND":
|
918
957
|
combined_result.valid = all(r.valid for r in results)
|
919
|
-
elif operator ==
|
958
|
+
elif operator == "OR":
|
920
959
|
combined_result.valid = any(r.valid for r in results)
|
921
960
|
if combined_result.valid:
|
922
961
|
# Clear errors if any validator passed
|
@@ -929,41 +968,44 @@ class CompositeValidator(BaseValidator):
|
|
929
968
|
class SchemaValidator(BaseValidator):
|
930
969
|
"""Validates against full schema - orchestrates other validators"""
|
931
970
|
|
932
|
-
def validate(
|
971
|
+
def validate(
|
972
|
+
self, value: Any, rule: ValidationRule, context: Dict[str, Any]
|
973
|
+
) -> ValidationResult:
|
933
974
|
"""Validate against complete schema"""
|
934
|
-
schema = rule.params.get(
|
935
|
-
config = value if isinstance(value, dict) else {
|
975
|
+
schema = rule.params.get("schema", {})
|
976
|
+
config = value if isinstance(value, dict) else {"value": value}
|
936
977
|
|
937
978
|
# Update context
|
938
979
|
context = context.copy()
|
939
|
-
context[
|
980
|
+
context["config"] = config
|
940
981
|
|
941
982
|
result = ValidationResult(valid=True)
|
942
983
|
strategy = ValidationStrategy()
|
943
984
|
|
944
985
|
# Apply each schema validation
|
945
|
-
if
|
986
|
+
if "type" in schema:
|
946
987
|
type_result = strategy.validate_type(config, schema)
|
947
988
|
self._merge_results(result, type_result)
|
948
989
|
|
949
|
-
if
|
990
|
+
if "required" in schema:
|
950
991
|
req_result = strategy.validate_required(config, schema)
|
951
992
|
self._merge_results(result, req_result)
|
952
993
|
|
953
|
-
if
|
954
|
-
for key, prop_schema in schema[
|
994
|
+
if "properties" in schema:
|
995
|
+
for key, prop_schema in schema["properties"].items():
|
955
996
|
if key in config:
|
956
997
|
prop_result = self.validate(
|
957
998
|
config[key],
|
958
999
|
ValidationRule(
|
959
|
-
type=ValidationType.SCHEMA,
|
960
|
-
params={'schema': prop_schema}
|
1000
|
+
type=ValidationType.SCHEMA, params={"schema": prop_schema}
|
961
1001
|
),
|
962
|
-
context
|
1002
|
+
context,
|
963
1003
|
)
|
964
1004
|
if not prop_result.valid:
|
965
1005
|
result.valid = False
|
966
|
-
result.errors.extend(
|
1006
|
+
result.errors.extend(
|
1007
|
+
[f"{key}: {e}" for e in prop_result.errors]
|
1008
|
+
)
|
967
1009
|
|
968
1010
|
return result
|
969
1011
|
|
@@ -999,7 +1041,7 @@ class ValidationStrategy(IConfigStrategy):
|
|
999
1041
|
ValidationType.RECURSIVE: RecursiveValidator(),
|
1000
1042
|
ValidationType.CROSS_FIELD: CrossFieldValidator(),
|
1001
1043
|
ValidationType.COMPOSITE: CompositeValidator(),
|
1002
|
-
ValidationType.SCHEMA: SchemaValidator()
|
1044
|
+
ValidationType.SCHEMA: SchemaValidator(),
|
1003
1045
|
}
|
1004
1046
|
|
1005
1047
|
def can_handle(self, source: Union[str, Path, Dict]) -> bool:
|
@@ -1015,7 +1057,7 @@ class ValidationStrategy(IConfigStrategy):
|
|
1015
1057
|
if not schema:
|
1016
1058
|
return True
|
1017
1059
|
|
1018
|
-
context = {
|
1060
|
+
context = {"config": config}
|
1019
1061
|
result = self._validate_with_schema(config, schema, context)
|
1020
1062
|
|
1021
1063
|
if not result.valid:
|
@@ -1029,30 +1071,32 @@ class ValidationStrategy(IConfigStrategy):
|
|
1029
1071
|
"""Transform config based on schema"""
|
1030
1072
|
return config
|
1031
1073
|
|
1032
|
-
def _validate_with_schema(
|
1074
|
+
def _validate_with_schema(
|
1075
|
+
self, config: Dict, schema: Dict, context: Dict
|
1076
|
+
) -> ValidationResult:
|
1033
1077
|
"""Validate configuration against schema"""
|
1034
1078
|
# Use schema validator for comprehensive validation
|
1035
1079
|
schema_rule = ValidationRule(
|
1036
|
-
type=ValidationType.SCHEMA,
|
1037
|
-
params={'schema': schema}
|
1080
|
+
type=ValidationType.SCHEMA, params={"schema": schema}
|
1038
1081
|
)
|
1039
1082
|
|
1040
|
-
return self.validators[ValidationType.SCHEMA].validate(
|
1083
|
+
return self.validators[ValidationType.SCHEMA].validate(
|
1084
|
+
config, schema_rule, context
|
1085
|
+
)
|
1041
1086
|
|
1042
1087
|
# Helper methods for direct validation
|
1043
1088
|
def validate_type(self, config: Dict, schema: Dict) -> ValidationResult:
|
1044
1089
|
"""Validate types in configuration"""
|
1045
1090
|
result = ValidationResult(valid=True)
|
1046
|
-
properties = schema.get(
|
1091
|
+
properties = schema.get("properties", {})
|
1047
1092
|
|
1048
1093
|
for key, prop_schema in properties.items():
|
1049
|
-
if key in config and
|
1094
|
+
if key in config and "type" in prop_schema:
|
1050
1095
|
rule = ValidationRule(
|
1051
|
-
type=ValidationType.TYPE,
|
1052
|
-
params={'type': prop_schema['type']}
|
1096
|
+
type=ValidationType.TYPE, params={"type": prop_schema["type"]}
|
1053
1097
|
)
|
1054
1098
|
prop_result = self.validators[ValidationType.TYPE].validate(
|
1055
|
-
config[key], rule, {
|
1099
|
+
config[key], rule, {"config": config}
|
1056
1100
|
)
|
1057
1101
|
if not prop_result.valid:
|
1058
1102
|
result.valid = False
|
@@ -1062,20 +1106,20 @@ class ValidationStrategy(IConfigStrategy):
|
|
1062
1106
|
|
1063
1107
|
def validate_required(self, config: Dict, schema: Dict) -> ValidationResult:
|
1064
1108
|
"""Validate required fields"""
|
1065
|
-
required_fields = schema.get(
|
1109
|
+
required_fields = schema.get("required", [])
|
1066
1110
|
if required_fields:
|
1067
1111
|
rule = ValidationRule(
|
1068
|
-
type=ValidationType.REQUIRED,
|
1069
|
-
params={'fields': required_fields}
|
1112
|
+
type=ValidationType.REQUIRED, params={"fields": required_fields}
|
1070
1113
|
)
|
1071
1114
|
return self.validators[ValidationType.REQUIRED].validate(
|
1072
|
-
None, rule, {
|
1115
|
+
None, rule, {"config": config}
|
1073
1116
|
)
|
1074
1117
|
|
1075
1118
|
return ValidationResult(valid=True)
|
1076
1119
|
|
1077
1120
|
def compose_validators(self, *validator_names: str) -> Callable:
|
1078
1121
|
"""Compose multiple validators into a single function"""
|
1122
|
+
|
1079
1123
|
def composed_validator(config: Dict, schema: Dict) -> ValidationResult:
|
1080
1124
|
result = ValidationResult(valid=True)
|
1081
1125
|
|
@@ -1084,7 +1128,7 @@ class ValidationStrategy(IConfigStrategy):
|
|
1084
1128
|
if val_type in self.validators:
|
1085
1129
|
validator = self.validators[val_type]
|
1086
1130
|
rule = ValidationRule(type=val_type, params=schema)
|
1087
|
-
val_result = validator.validate(config, rule, {
|
1131
|
+
val_result = validator.validate(config, rule, {"config": config})
|
1088
1132
|
|
1089
1133
|
if not val_result.valid:
|
1090
1134
|
result.valid = False
|
@@ -1097,9 +1141,4 @@ class ValidationStrategy(IConfigStrategy):
|
|
1097
1141
|
|
1098
1142
|
|
1099
1143
|
# Export main components
|
1100
|
-
__all__ = [
|
1101
|
-
'ValidationStrategy',
|
1102
|
-
'ValidationRule',
|
1103
|
-
'ValidationResult',
|
1104
|
-
'ValidationType'
|
1105
|
-
]
|
1144
|
+
__all__ = ["ValidationResult", "ValidationRule", "ValidationStrategy", "ValidationType"]
|