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