"""
SLOTH Validation
Validation exception classes, the :class:`ValidatorPlugin` that powers
per-category and cross-category checks, and :class:`CategoryValidator` (the
chainable wrapper).
"""
from enum import Enum, auto
from typing import Any, Callable, Dict, List, Tuple, Optional, TYPE_CHECKING
from .plugins import Plugin, PluginWrapper
if TYPE_CHECKING:
from .models import Category
[docs]
class ValidationSeverity(Enum):
"""Severity levels for validation errors."""
ERROR = auto() # Validation failures that should prevent processing
WARNING = auto() # Issues that should be flagged but don't prevent processing
INFO = auto() # Informational notices
[docs]
class ValidationError(Exception):
"""Exception raised for validation errors."""
[docs]
def __init__(
self,
message: str,
path: str = "",
severity: ValidationSeverity = ValidationSeverity.ERROR,
):
"""
Initialize validation error.
Args:
message: Error message
path: Path where the error occurred (e.g., JSON path, category name)
severity: Validation error severity
"""
self.message = message
self.path = path
self.severity = severity
if path:
super().__init__(f"{path}: {message}")
else:
super().__init__(message)
# ---------------------------------------------------------------------------
# Validation plugin
# ---------------------------------------------------------------------------
[docs]
class ValidatorPlugin(Plugin):
"""Plugin for per-category validation with cross-checker support.
Multiple validators can be registered for the same category β they
will all run in registration order.
"""
[docs]
def __init__(self):
self._validators: Dict[str, List[Callable]] = {}
self._cross_checkers: Dict[Tuple[str, str], List[Callable]] = {}
# -- registration helpers -----------------------------------------------
[docs]
def register_validator(
self, category_name: str, validator_function: Callable
) -> None:
"""Register a validator callable for a category name.
Multiple validators for the same category are allowed.
"""
self._validators.setdefault(category_name, []).append(validator_function)
[docs]
def register_cross_checker(
self,
category_pair: Tuple[str, str],
cross_checker_function: Callable,
) -> None:
"""Register a cross-checker callable for a pair of category names."""
self._cross_checkers.setdefault(category_pair, []).append(cross_checker_function)
# -- lookup helpers -----------------------------------------------------
[docs]
def get_validators(self, category_name: str) -> List[Callable]:
"""Return all validators for *category_name*."""
return self._validators.get(category_name, [])
[docs]
def get_cross_checkers(
self, category_pair: Tuple[str, str]
) -> List[Callable]:
"""Return all cross-checkers for *category_pair*."""
return self._cross_checkers.get(category_pair, [])
# -- Plugin interface ---------------------------------------------------
[docs]
def create_wrapper(self, target) -> "CategoryValidator":
return CategoryValidator(target, self)
[docs]
def execute(self, target, *args, **kwargs) -> Any:
results = []
for validator in self._validators.get(target.name, []):
result = validator(target)
if result is not None:
results.append(result)
return results or None
[docs]
class CategoryValidator(PluginWrapper):
"""Chainable wrapper for category validation with cross-checking."""
_plugin: "ValidatorPlugin"
def __call__(self) -> "CategoryValidator":
"""Execute the registered validator for this category."""
super().__call__()
return self
[docs]
def against(self, other_category: "Category") -> "CategoryValidator":
"""Execute cross-validation against *other_category*."""
for cross_checker in self._plugin.get_cross_checkers(
(self._target.name, other_category.name)
):
cross_checker(self._target, other_category)
return self