Initial upload: ConfDoc v0.1.0 - Config validation and documentation generator
This commit is contained in:
127
src/confdoc/validator/validator.py
Normal file
127
src/confdoc/validator/validator.py
Normal file
@@ -0,0 +1,127 @@
|
||||
from typing import Any, Dict, List, Optional
|
||||
from jsonschema import validate, ValidationError as JsonSchemaValidationError
|
||||
from jsonschema.exceptions import best_match
|
||||
|
||||
from confdoc.validator.errors import ValidationError as CustomValidationError, ErrorFormatter, ErrorSeverity
|
||||
|
||||
|
||||
class SchemaValidator:
|
||||
"""Validates configuration against JSON Schema with detailed error reporting."""
|
||||
|
||||
def __init__(self, draft: str = "draft-07"):
|
||||
"""Initialize validator with schema draft."""
|
||||
self.draft = draft
|
||||
|
||||
def validate(self, config: Dict[str, Any], schema: Dict[str, Any]) -> tuple:
|
||||
"""
|
||||
Validate configuration against schema.
|
||||
|
||||
Returns:
|
||||
tuple: (is_valid: bool, errors: List[CustomValidationError])
|
||||
"""
|
||||
errors = []
|
||||
|
||||
try:
|
||||
validate(instance=config, schema=schema, cls=_get_validator_class(self.draft))
|
||||
except JsonSchemaValidationError as e:
|
||||
errors = self._process_validation_error(e, config)
|
||||
|
||||
return (len(errors) == 0, errors)
|
||||
|
||||
def validate_multiple(self, configs: List[Dict[str, Any]], schema: Dict[str, Any]) -> Dict[int, List[CustomValidationError]]:
|
||||
"""Validate multiple configurations and return errors for each."""
|
||||
results = {}
|
||||
for i, config in enumerate(configs):
|
||||
is_valid, errors = self.validate(config, schema)
|
||||
if not is_valid:
|
||||
results[i] = errors
|
||||
return results
|
||||
|
||||
def _process_validation_error(
|
||||
self,
|
||||
error: JsonSchemaValidationError,
|
||||
config: Dict[str, Any]
|
||||
) -> List[CustomValidationError]:
|
||||
"""Process jsonschema ValidationError into CustomValidationError objects."""
|
||||
errors = []
|
||||
|
||||
for schema_error in error.context:
|
||||
error_info = self._extract_error_info(schema_error)
|
||||
|
||||
severity = ErrorFormatter.classify_severity(error_info)
|
||||
suggestion = ErrorFormatter.generate_suggestion(error_info)
|
||||
|
||||
custom_error = CustomValidationError(
|
||||
message=schema_error.message,
|
||||
path=self._format_path(schema_error.json_path),
|
||||
line=error_info.get("line"),
|
||||
column=error_info.get("column"),
|
||||
severity=severity,
|
||||
suggestion=suggestion,
|
||||
validator=error_info.get("validator"),
|
||||
)
|
||||
|
||||
errors.append(custom_error)
|
||||
|
||||
if not errors:
|
||||
errors = [
|
||||
CustomValidationError(
|
||||
message=error.message,
|
||||
path=self._format_path(error.json_path),
|
||||
severity=ErrorSeverity.ERROR,
|
||||
)
|
||||
]
|
||||
|
||||
return errors
|
||||
|
||||
def _extract_error_info(self, error: JsonSchemaValidationError) -> Dict[str, Any]:
|
||||
"""Extract detailed information from a validation error."""
|
||||
info = {
|
||||
"validator": getattr(error, "validator", None),
|
||||
"validator_value": getattr(error, "validator_value", None),
|
||||
"message": error.message,
|
||||
}
|
||||
|
||||
if hasattr(error, "absolute_schema_path"):
|
||||
schema_path = list(error.absolute_schema_path)
|
||||
if schema_path:
|
||||
info["validator"] = schema_path[-1] if isinstance(schema_path[-1], str) else info["validator"]
|
||||
|
||||
return info
|
||||
|
||||
def _format_path(self, json_path: str) -> str:
|
||||
"""Format JSON path to dot notation."""
|
||||
if not json_path or json_path == "$":
|
||||
return ""
|
||||
|
||||
path = json_path.replace("$.", "").replace(".", ".")
|
||||
return path
|
||||
|
||||
def get_best_match(self, config: Dict[str, Any], schema: Dict[str, Any]) -> Optional[CustomValidationError]:
|
||||
"""Get the most relevant validation error."""
|
||||
try:
|
||||
validate(instance=config, schema=schema, cls=_get_validator_class(self.draft))
|
||||
return None
|
||||
except JsonSchemaValidationError as e:
|
||||
best = best_match(e.context)
|
||||
if best:
|
||||
return CustomValidationError(
|
||||
message=best.message,
|
||||
path=self._format_path(best.json_path),
|
||||
severity=ErrorFormatter.classify_severity(
|
||||
{"validator": getattr(best, "validator", None)}
|
||||
),
|
||||
)
|
||||
return CustomValidationError(message=e.message)
|
||||
|
||||
|
||||
def _get_validator_class(draft: str):
|
||||
"""Get the appropriate validator class for the draft."""
|
||||
from jsonschema import Draft7Validator, Draft201909Validator
|
||||
|
||||
validators = {
|
||||
"draft-07": Draft7Validator,
|
||||
"2019-09": Draft201909Validator,
|
||||
}
|
||||
|
||||
return validators.get(draft, Draft7Validator)
|
||||
Reference in New Issue
Block a user