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