# -*- coding: utf-8 -*- """ Validator-Klassen für verschiedene Validierungsszenarien. Bietet eine erweiterbare Hierarchie von Validatoren für Typen, Bereiche, Patterns und komplexe Strukturen. """ from __future__ import annotations import re from abc import ABC, abstractmethod from dataclasses import dataclass from enum import Enum from typing import Any, Callable, Generic, Sequence, Type, TypeVar from trixy_core.utils.validation.errors import ( FieldError, PatternValidationError, RangeValidationError, RequiredFieldError, TypeValidationError, ValidationError, ) T = TypeVar("T") class Validator(ABC, Generic[T]): """ Abstrakte Basisklasse für alle Validatoren. Definiert die Schnittstelle für Validierung und ermöglicht Verkettung von Validatoren. """ def __init__( self, message: str | None = None, code: str = "invalid", ) -> None: """ Initialisiert den Validator. Args: message: Optionale benutzerdefinierte Fehlermeldung. code: Fehlercode für programmatische Behandlung. """ self._message = message self._code = code @property def message(self) -> str: """Gibt die Fehlermeldung zurück.""" return self._message or self.default_message @property def default_message(self) -> str: """Standard-Fehlermeldung (kann überschrieben werden).""" return "Validierung fehlgeschlagen" @abstractmethod def validate(self, value: T, field: str = "value") -> None: """ Validiert einen Wert. Args: value: Der zu validierende Wert. field: Feldname für Fehlermeldungen. Raises: ValidationError: Wenn Validierung fehlschlägt. """ pass def is_valid(self, value: T) -> bool: """ Prüft, ob ein Wert gültig ist. Args: value: Der zu prüfende Wert. Returns: True wenn gültig. """ try: self.validate(value) return True except ValidationError: return False def __call__(self, value: T, field: str = "value") -> None: """Ermöglicht Aufruf als Funktion.""" self.validate(value, field) def __and__(self, other: "Validator") -> "AllValidator": """Kombiniert mit AND-Logik.""" return AllValidator([self, other]) def __or__(self, other: "Validator") -> "AnyValidator": """Kombiniert mit OR-Logik.""" return AnyValidator([self, other]) class TypeValidator(Validator[Any]): """ Validator für Typ-Prüfung. Prüft, ob ein Wert einer bestimmten Typ-Klasse entspricht. """ def __init__( self, expected_type: Type | tuple[Type, ...], allow_none: bool = False, message: str | None = None, ) -> None: """ Initialisiert den TypeValidator. Args: expected_type: Erwarteter Typ oder Tuple von Typen. allow_none: Ob None erlaubt ist. message: Optionale Fehlermeldung. """ super().__init__(message=message, code="type_error") self.expected_type = expected_type self.allow_none = allow_none @property def default_message(self) -> str: """Standard-Fehlermeldung.""" if isinstance(self.expected_type, tuple): type_names = " oder ".join(t.__name__ for t in self.expected_type) else: type_names = self.expected_type.__name__ return f"Muss vom Typ {type_names} sein" def validate(self, value: Any, field: str = "value") -> None: """Validiert den Typ des Wertes.""" if value is None: if self.allow_none: return raise ValidationError( message="Wert darf nicht None sein", field=field, value=value, code="null_error", ) if not isinstance(value, self.expected_type): raise TypeValidationError( field=field, expected_type=self.expected_type, # type: ignore actual_type=type(value), value=value, ) class RangeValidator(Validator[Any]): """ Validator für numerische Bereiche. Prüft, ob ein Wert innerhalb eines definierten Bereichs liegt. """ def __init__( self, min_value: Any | None = None, max_value: Any | None = None, inclusive_min: bool = True, inclusive_max: bool = True, message: str | None = None, ) -> None: """ Initialisiert den RangeValidator. Args: min_value: Minimaler Wert (None = kein Minimum). max_value: Maximaler Wert (None = kein Maximum). inclusive_min: Ob Minimum inklusive ist. inclusive_max: Ob Maximum inklusive ist. message: Optionale Fehlermeldung. """ super().__init__(message=message, code="range_error") self.min_value = min_value self.max_value = max_value self.inclusive_min = inclusive_min self.inclusive_max = inclusive_max @property def default_message(self) -> str: """Standard-Fehlermeldung.""" parts = [] if self.min_value is not None: op = ">=" if self.inclusive_min else ">" parts.append(f"{op} {self.min_value}") if self.max_value is not None: op = "<=" if self.inclusive_max else "<" parts.append(f"{op} {self.max_value}") return f"Wert muss {' und '.join(parts)} sein" def validate(self, value: Any, field: str = "value") -> None: """Validiert, ob der Wert im Bereich liegt.""" if self.min_value is not None: if self.inclusive_min: if value < self.min_value: raise RangeValidationError( field=field, value=value, min_value=self.min_value, max_value=self.max_value, ) else: if value <= self.min_value: raise RangeValidationError( field=field, value=value, min_value=self.min_value, max_value=self.max_value, ) if self.max_value is not None: if self.inclusive_max: if value > self.max_value: raise RangeValidationError( field=field, value=value, min_value=self.min_value, max_value=self.max_value, ) else: if value >= self.max_value: raise RangeValidationError( field=field, value=value, min_value=self.min_value, max_value=self.max_value, ) class PatternValidator(Validator[str]): """ Validator für Regex-Pattern. Prüft, ob ein String einem regulären Ausdruck entspricht. """ def __init__( self, pattern: str, flags: int = 0, message: str | None = None, ) -> None: """ Initialisiert den PatternValidator. Args: pattern: Regulärer Ausdruck. flags: Regex-Flags (z.B. re.IGNORECASE). message: Optionale Fehlermeldung. """ super().__init__(message=message, code="pattern_error") self.pattern = pattern self._regex = re.compile(pattern, flags) @property def default_message(self) -> str: """Standard-Fehlermeldung.""" return f"Muss dem Pattern '{self.pattern}' entsprechen" def validate(self, value: str, field: str = "value") -> None: """Validiert, ob der String dem Pattern entspricht.""" if not self._regex.match(value): raise PatternValidationError( field=field, value=value, pattern=self.pattern, ) class LengthValidator(Validator[Any]): """ Validator für Längen-Prüfung. Prüft die Länge von Strings, Listen, etc. """ def __init__( self, min_length: int | None = None, max_length: int | None = None, exact_length: int | None = None, message: str | None = None, ) -> None: """ Initialisiert den LengthValidator. Args: min_length: Minimale Länge. max_length: Maximale Länge. exact_length: Exakte Länge (überschreibt min/max). message: Optionale Fehlermeldung. """ super().__init__(message=message, code="length_error") self.min_length = min_length self.max_length = max_length self.exact_length = exact_length @property def default_message(self) -> str: """Standard-Fehlermeldung.""" if self.exact_length is not None: return f"Länge muss genau {self.exact_length} sein" parts = [] if self.min_length is not None: parts.append(f"mindestens {self.min_length}") if self.max_length is not None: parts.append(f"höchstens {self.max_length}") return f"Länge muss {' und '.join(parts)} sein" def validate(self, value: Any, field: str = "value") -> None: """Validiert die Länge des Wertes.""" try: length = len(value) except TypeError: raise ValidationError( message="Wert hat keine Länge", field=field, value=value, code="no_length", ) if self.exact_length is not None: if length != self.exact_length: raise ValidationError( message=f"Länge muss genau {self.exact_length} sein, ist aber {length}", field=field, value=value, code="length_error", ) else: if self.min_length is not None and length < self.min_length: raise ValidationError( message=f"Länge muss mindestens {self.min_length} sein, ist aber {length}", field=field, value=value, code="min_length_error", ) if self.max_length is not None and length > self.max_length: raise ValidationError( message=f"Länge darf höchstens {self.max_length} sein, ist aber {length}", field=field, value=value, code="max_length_error", ) class RequiredValidator(Validator[Any]): """ Validator für Pflichtfelder. Prüft, ob ein Wert vorhanden und nicht leer ist. """ def __init__( self, allow_empty: bool = False, allow_whitespace: bool = False, message: str | None = None, ) -> None: """ Initialisiert den RequiredValidator. Args: allow_empty: Ob leere Werte ([], {}, "") erlaubt sind. allow_whitespace: Ob Whitespace-only Strings erlaubt sind. message: Optionale Fehlermeldung. """ super().__init__(message=message, code="required") self.allow_empty = allow_empty self.allow_whitespace = allow_whitespace @property def default_message(self) -> str: """Standard-Fehlermeldung.""" return "Dieses Feld ist erforderlich" def validate(self, value: Any, field: str = "value") -> None: """Validiert, ob der Wert vorhanden ist.""" if value is None: raise RequiredFieldError(field) if not self.allow_empty: try: if len(value) == 0: raise ValidationError( message="Darf nicht leer sein", field=field, value=value, code="empty", ) except TypeError: pass # Hat keine Länge, ist OK if not self.allow_whitespace and isinstance(value, str): if not value.strip(): raise ValidationError( message="Darf nicht nur Leerzeichen enthalten", field=field, value=value, code="whitespace_only", ) class OptionalValidator(Validator[Any]): """ Validator für optionale Felder mit Fallback. Erlaubt None und wendet sonst den inneren Validator an. """ def __init__( self, inner_validator: Validator, default: Any = None, ) -> None: """ Initialisiert den OptionalValidator. Args: inner_validator: Validator für nicht-None Werte. default: Standardwert falls None. """ super().__init__() self.inner_validator = inner_validator self.default = default def validate(self, value: Any, field: str = "value") -> None: """Validiert den Wert falls nicht None.""" if value is None: return self.inner_validator.validate(value, field) class EnumValidator(Validator[Any]): """ Validator für Enum-Werte. Prüft, ob ein Wert ein gültiger Enum-Wert ist. """ def __init__( self, enum_class: Type[Enum] | None = None, allowed_values: Sequence[Any] | None = None, message: str | None = None, ) -> None: """ Initialisiert den EnumValidator. Args: enum_class: Enum-Klasse für gültige Werte. allowed_values: Alternative: Liste erlaubter Werte. message: Optionale Fehlermeldung. """ super().__init__(message=message, code="enum_error") self.enum_class = enum_class self.allowed_values = allowed_values @property def default_message(self) -> str: """Standard-Fehlermeldung.""" if self.enum_class: values = [e.value for e in self.enum_class] else: values = list(self.allowed_values or []) return f"Muss einer von {values} sein" def validate(self, value: Any, field: str = "value") -> None: """Validiert, ob der Wert erlaubt ist.""" if self.enum_class: valid_values = [e.value for e in self.enum_class] valid_names = [e.name for e in self.enum_class] if value not in valid_values and value not in valid_names: if not isinstance(value, self.enum_class): raise ValidationError( message=self.message, field=field, value=value, code="enum_error", ) elif self.allowed_values: if value not in self.allowed_values: raise ValidationError( message=self.message, field=field, value=value, code="enum_error", ) class ListValidator(Validator[list]): """ Validator für Listen. Validiert Listen und deren Elemente. """ def __init__( self, item_validator: Validator | None = None, min_items: int | None = None, max_items: int | None = None, unique: bool = False, message: str | None = None, ) -> None: """ Initialisiert den ListValidator. Args: item_validator: Validator für jedes Element. min_items: Minimale Anzahl Elemente. max_items: Maximale Anzahl Elemente. unique: Ob alle Elemente eindeutig sein müssen. message: Optionale Fehlermeldung. """ super().__init__(message=message, code="list_error") self.item_validator = item_validator self.min_items = min_items self.max_items = max_items self.unique = unique def validate(self, value: list, field: str = "value") -> None: """Validiert die Liste und deren Elemente.""" if not isinstance(value, (list, tuple)): raise TypeValidationError( field=field, expected_type=list, actual_type=type(value), value=value, ) # Längen-Prüfung if self.min_items is not None and len(value) < self.min_items: raise ValidationError( message=f"Liste muss mindestens {self.min_items} Elemente haben", field=field, value=value, code="min_items", ) if self.max_items is not None and len(value) > self.max_items: raise ValidationError( message=f"Liste darf höchstens {self.max_items} Elemente haben", field=field, value=value, code="max_items", ) # Eindeutigkeits-Prüfung if self.unique: try: if len(set(value)) != len(value): raise ValidationError( message="Liste enthält doppelte Elemente", field=field, value=value, code="not_unique", ) except TypeError: # Elemente nicht hashbar pass # Element-Validierung if self.item_validator: for i, item in enumerate(value): try: self.item_validator.validate(item, f"{field}[{i}]") except ValidationError as e: raise ValidationError( message=f"Element {i}: {e.message}", field=f"{field}[{i}]", value=item, code="item_error", ) class DictValidator(Validator[dict]): """ Validator für Dictionaries. Validiert Dictionaries auf Schlüssel und Werte. """ def __init__( self, key_validator: Validator | None = None, value_validator: Validator | None = None, required_keys: Sequence[str] | None = None, allowed_keys: Sequence[str] | None = None, message: str | None = None, ) -> None: """ Initialisiert den DictValidator. Args: key_validator: Validator für Schlüssel. value_validator: Validator für Werte. required_keys: Liste erforderlicher Schlüssel. allowed_keys: Liste erlaubter Schlüssel (andere werden abgelehnt). message: Optionale Fehlermeldung. """ super().__init__(message=message, code="dict_error") self.key_validator = key_validator self.value_validator = value_validator self.required_keys = set(required_keys) if required_keys else set() self.allowed_keys = set(allowed_keys) if allowed_keys else None def validate(self, value: dict, field: str = "value") -> None: """Validiert das Dictionary.""" if not isinstance(value, dict): raise TypeValidationError( field=field, expected_type=dict, actual_type=type(value), value=value, ) # Erforderliche Schlüssel prüfen missing = self.required_keys - set(value.keys()) if missing: raise ValidationError( message=f"Fehlende Schlüssel: {missing}", field=field, value=value, code="missing_keys", ) # Erlaubte Schlüssel prüfen if self.allowed_keys is not None: extra = set(value.keys()) - self.allowed_keys if extra: raise ValidationError( message=f"Unerlaubte Schlüssel: {extra}", field=field, value=value, code="extra_keys", ) # Schlüssel und Werte validieren for key, val in value.items(): if self.key_validator: self.key_validator.validate(key, f"{field}[key]") if self.value_validator: self.value_validator.validate(val, f"{field}[{key!r}]") class CompositeValidator(Validator[Any]): """ Basis für zusammengesetzte Validatoren. """ def __init__( self, validators: Sequence[Validator], message: str | None = None, ) -> None: """ Initialisiert den CompositeValidator. Args: validators: Liste von Validatoren. message: Optionale Fehlermeldung. """ super().__init__(message=message) self.validators = list(validators) def add(self, validator: Validator) -> "CompositeValidator": """Fügt einen Validator hinzu.""" self.validators.append(validator) return self class AllValidator(CompositeValidator): """ Validator der alle Unter-Validatoren prüft (AND-Logik). """ def validate(self, value: Any, field: str = "value") -> None: """Validiert mit allen Validatoren.""" for validator in self.validators: validator.validate(value, field) class AnyValidator(CompositeValidator): """ Validator der mindestens einen Unter-Validator prüft (OR-Logik). """ @property def default_message(self) -> str: """Standard-Fehlermeldung.""" return "Keiner der Validatoren war erfolgreich" def validate(self, value: Any, field: str = "value") -> None: """Validiert mit mindestens einem Validator.""" errors = [] for validator in self.validators: try: validator.validate(value, field) return # Mindestens einer erfolgreich except ValidationError as e: errors.append(e) # Alle fehlgeschlagen raise ValidationError( message=self.message, field=field, value=value, code="all_failed", ) class ConditionalValidator(Validator[Any]): """ Validator mit bedingter Logik. Wendet einen Validator nur an, wenn eine Bedingung erfüllt ist. """ def __init__( self, condition: Callable[[Any], bool], then_validator: Validator, else_validator: Validator | None = None, ) -> None: """ Initialisiert den ConditionalValidator. Args: condition: Funktion, die bestimmt welcher Validator verwendet wird. then_validator: Validator wenn Bedingung True. else_validator: Optionaler Validator wenn Bedingung False. """ super().__init__() self.condition = condition self.then_validator = then_validator self.else_validator = else_validator def validate(self, value: Any, field: str = "value") -> None: """Validiert basierend auf der Bedingung.""" if self.condition(value): self.then_validator.validate(value, field) elif self.else_validator: self.else_validator.validate(value, field) class CallableValidator(Validator[Any]): """ Validator basierend auf einer benutzerdefinierten Funktion. """ def __init__( self, check: Callable[[Any], bool], message: str = "Validierung fehlgeschlagen", code: str = "custom", ) -> None: """ Initialisiert den CallableValidator. Args: check: Funktion, die True zurückgibt wenn gültig. message: Fehlermeldung bei Ungültigkeit. code: Fehlercode. """ super().__init__(message=message, code=code) self.check = check def validate(self, value: Any, field: str = "value") -> None: """Validiert mit der benutzerdefinierten Funktion.""" if not self.check(value): raise ValidationError( message=self.message, field=field, value=value, code=self._code, )