|
18 | 18 | from ..reporters.markdown import MarkdownReporter |
19 | 19 | from ..services.research_loader import ResearchLoader |
20 | 20 | from ..services.scanner import Scanner |
21 | | -from ..utils.security import validate_config_dict, validate_path |
| 21 | +from pydantic import ValidationError |
22 | 22 | from ..utils.subprocess_utils import safe_subprocess_run |
23 | 23 | from .align import align |
24 | 24 | from .assess_batch import assess_batch |
@@ -242,73 +242,45 @@ def run_assessment(repository_path, verbose, output_dir, config_path): |
242 | 242 |
|
243 | 243 |
|
244 | 244 | def load_config(config_path: Path) -> Config: |
245 | | - """Load configuration from YAML file with validation. |
| 245 | + """Load configuration from YAML file with Pydantic validation. |
246 | 246 |
|
247 | | - Security: Validates YAML structure to prevent injection attacks |
248 | | - and malformed data from causing crashes or unexpected behavior. |
249 | | - Uses centralized security utilities from utils.security module. |
| 247 | + Uses Pydantic for automatic validation, replacing 67 lines of manual |
| 248 | + validation code with declarative field validators. |
| 249 | +
|
| 250 | + Security: Uses yaml.safe_load() for safe YAML parsing and Pydantic |
| 251 | + validators for type checking and path sanitization. |
| 252 | +
|
| 253 | + Args: |
| 254 | + config_path: Path to YAML configuration file |
| 255 | +
|
| 256 | + Returns: |
| 257 | + Validated Config instance |
| 258 | +
|
| 259 | + Raises: |
| 260 | + ValidationError: If YAML data doesn't match expected schema |
| 261 | + FileNotFoundError: If config file doesn't exist |
| 262 | + yaml.YAMLError: If YAML parsing fails |
250 | 263 | """ |
251 | 264 | import yaml |
252 | 265 |
|
253 | | - with open(config_path, "r", encoding="utf-8") as f: |
254 | | - data = yaml.safe_load(f) |
255 | | - |
256 | | - # Define config schema for validation |
257 | | - schema = { |
258 | | - "weights": {str: (int, float)}, # dict[str, int|float] |
259 | | - "excluded_attributes": [str], # list[str] |
260 | | - "language_overrides": { |
261 | | - str: list |
262 | | - }, # dict[str, list] (nested list validated separately) |
263 | | - "output_dir": str, |
264 | | - "report_theme": str, |
265 | | - "custom_theme": dict, # dict (nested types validated separately) |
266 | | - } |
267 | | - |
268 | | - # Validate config structure using centralized utility |
269 | | - validated = validate_config_dict(data, schema) |
270 | | - |
271 | | - # Additional nested validations for complex types |
272 | | - if "language_overrides" in validated: |
273 | | - lang_overrides = validated["language_overrides"] |
274 | | - for lang, patterns in lang_overrides.items(): |
275 | | - if not isinstance(patterns, list): |
276 | | - raise ValueError( |
277 | | - f"'language_overrides' values must be lists, got {type(patterns).__name__}" |
278 | | - ) |
279 | | - for pattern in patterns: |
280 | | - if not isinstance(pattern, str): |
281 | | - raise ValueError( |
282 | | - f"'language_overrides' patterns must be strings, got {type(pattern).__name__}" |
283 | | - ) |
284 | | - |
285 | | - if "custom_theme" in validated: |
286 | | - custom_theme = validated["custom_theme"] |
287 | | - for key, value in custom_theme.items(): |
288 | | - if not isinstance(key, str): |
289 | | - raise ValueError( |
290 | | - f"'custom_theme' keys must be strings, got {type(key).__name__}" |
291 | | - ) |
292 | | - if not isinstance(value, str): |
293 | | - raise ValueError( |
294 | | - f"'custom_theme' values must be strings, got {type(value).__name__}" |
295 | | - ) |
296 | | - |
297 | | - # Validate and sanitize output_dir path |
298 | | - output_dir = None |
299 | | - if "output_dir" in validated: |
300 | | - output_dir = validate_path( |
301 | | - validated["output_dir"], allow_system_dirs=False, must_exist=False |
302 | | - ) |
303 | | - |
304 | | - return Config( |
305 | | - weights=validated.get("weights", {}), |
306 | | - excluded_attributes=validated.get("excluded_attributes", []), |
307 | | - language_overrides=validated.get("language_overrides", {}), |
308 | | - output_dir=output_dir, |
309 | | - report_theme=validated.get("report_theme", "default"), |
310 | | - custom_theme=validated.get("custom_theme"), |
311 | | - ) |
| 266 | + try: |
| 267 | + with open(config_path, "r", encoding="utf-8") as f: |
| 268 | + data = yaml.safe_load(f) |
| 269 | + |
| 270 | + # Pydantic handles all validation automatically |
| 271 | + return Config.from_yaml_dict(data) |
| 272 | + except ValidationError as e: |
| 273 | + # Convert Pydantic validation errors to user-friendly messages |
| 274 | + errors = [] |
| 275 | + for error in e.errors(): |
| 276 | + field = " → ".join(str(x) for x in error["loc"]) |
| 277 | + msg = error["msg"] |
| 278 | + errors.append(f" - {field}: {msg}") |
| 279 | + |
| 280 | + click.echo("Configuration validation failed:", err=True) |
| 281 | + for error in errors: |
| 282 | + click.echo(error, err=True) |
| 283 | + sys.exit(1) |
312 | 284 |
|
313 | 285 |
|
314 | 286 | @cli.command() |
|
0 commit comments