# -*- coding: utf-8 -*- """ Validierungs-Middleware für Events. Validiert Event-Daten vor der Verarbeitung. """ from __future__ import annotations from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Callable, Type from trixy_core.events.middleware.base import ( EventMiddleware, MiddlewareAction, MiddlewareContext, MiddlewareResult, ) if TYPE_CHECKING: from trixy_core.events.event_data.base import EventData class EventValidationError(Exception): """ Fehler bei der Event-Validierung. """ def __init__( self, message: str, event_name: str, field: str | None = None, value: Any = None, ) -> None: """ Initialisiert den Fehler. Args: message: Fehlermeldung. event_name: Name des Events. field: Betroffenes Feld. value: Ungültiger Wert. """ super().__init__(message) self.event_name = event_name self.field = field self.value = value @dataclass class ValidationRule: """ Regel für Event-Validierung. """ field: str """Feldname (oder "*" für Event-Level).""" validator: Callable[[Any], bool] """Validierungsfunktion.""" message: str = "Validierung fehlgeschlagen" """Fehlermeldung.""" required: bool = False """Ob das Feld vorhanden sein muss.""" allow_none: bool = True """Ob None erlaubt ist.""" @dataclass class EventSchema: """ Schema für Event-Validierung. """ event_name: str """Name des Events (oder "*" für alle).""" rules: list[ValidationRule] = field(default_factory=list) """Validierungsregeln.""" event_class: Type["EventData"] | None = None """Optionale Event-Klasse für Typ-Prüfung.""" custom_validator: Callable[["EventData"], bool] | None = None """Optionale benutzerdefinierte Validierungsfunktion.""" def add_rule( self, field: str, validator: Callable[[Any], bool], message: str = "Validierung fehlgeschlagen", required: bool = False, allow_none: bool = True, ) -> "EventSchema": """ Fügt eine Regel hinzu (Fluent-API). Args: field: Feldname. validator: Validierungsfunktion. message: Fehlermeldung. required: Ob erforderlich. allow_none: Ob None erlaubt. Returns: Self für Verkettung. """ self.rules.append(ValidationRule( field=field, validator=validator, message=message, required=required, allow_none=allow_none, )) return self class ValidationMiddleware(EventMiddleware): """ Middleware für Event-Validierung. Validiert Event-Daten gegen definierte Schemas und Regeln. Example: middleware = ValidationMiddleware() # Schema definieren schema = EventSchema("user_login") schema.add_rule("username", lambda x: len(x) >= 3, "Username zu kurz") schema.add_rule("password", lambda x: len(x) >= 8, "Passwort zu kurz") middleware.register_schema(schema) # Oder mit Fluent-API middleware.for_event("user_register") \\ .require("email") \\ .rule("email", lambda x: "@" in x, "Ungültige E-Mail") \\ .rule("age", lambda x: x >= 18, "Zu jung") """ def __init__( self, name: str = "ValidationMiddleware", enabled: bool = True, strict: bool = True, on_validation_error: Callable[[EventValidationError], None] | None = None, ) -> None: """ Initialisiert die Middleware. Args: name: Name der Middleware. enabled: Ob aktiv. strict: Ob bei Fehlern abgebrochen wird. on_validation_error: Callback bei Validierungsfehlern. """ super().__init__(name, enabled) self._strict = strict self._on_validation_error = on_validation_error self._schemas: dict[str, EventSchema] = {} self._global_rules: list[ValidationRule] = [] def register_schema(self, schema: EventSchema) -> None: """ Registriert ein Event-Schema. Args: schema: Das Schema. """ self._schemas[schema.event_name] = schema def for_event(self, event_name: str) -> "SchemaBuilder": """ Startet einen Schema-Builder für ein Event. Args: event_name: Name des Events. Returns: SchemaBuilder für Fluent-API. """ schema = EventSchema(event_name=event_name) self._schemas[event_name] = schema return SchemaBuilder(schema, self) def add_global_rule( self, field: str, validator: Callable[[Any], bool], message: str = "Validierung fehlgeschlagen", ) -> None: """ Fügt eine globale Regel hinzu. Args: field: Feldname. validator: Validierungsfunktion. message: Fehlermeldung. """ self._global_rules.append(ValidationRule( field=field, validator=validator, message=message, )) def _get_field_value( self, event_data: "EventData", field: str, ) -> tuple[bool, Any]: """ Holt einen Feldwert aus den Event-Daten. Args: event_data: Die Event-Daten. field: Feldname. Returns: Tupel (gefunden, Wert). """ # Direktes Attribut if hasattr(event_data, field): return True, getattr(event_data, field) # In Metadata if field in event_data.metadata: return True, event_data.metadata[field] return False, None def _validate_rule( self, event_data: "EventData", event_name: str, rule: ValidationRule, ) -> EventValidationError | None: """ Validiert eine einzelne Regel. Args: event_data: Die Event-Daten. event_name: Name des Events. rule: Die Regel. Returns: Fehler oder None. """ if rule.field == "*": # Event-Level-Validierung try: if not rule.validator(event_data): return EventValidationError( rule.message, event_name, ) except Exception as e: return EventValidationError( f"Validierungsfehler: {e}", event_name, ) return None found, value = self._get_field_value(event_data, rule.field) # Required-Prüfung if rule.required and not found: return EventValidationError( f"Pflichtfeld fehlt: {rule.field}", event_name, field=rule.field, ) if not found: return None # None-Prüfung if value is None: if not rule.allow_none: return EventValidationError( f"Feld darf nicht None sein: {rule.field}", event_name, field=rule.field, value=value, ) return None # Validator ausführen try: if not rule.validator(value): return EventValidationError( rule.message, event_name, field=rule.field, value=value, ) except Exception as e: return EventValidationError( f"Validierungsfehler ({rule.field}): {e}", event_name, field=rule.field, value=value, ) return None def _validate_event( self, event_name: str, event_data: "EventData", ) -> list[EventValidationError]: """ Validiert ein Event. Args: event_name: Name des Events. event_data: Die Event-Daten. Returns: Liste von Fehlern. """ errors: list[EventValidationError] = [] # Globale Regeln for rule in self._global_rules: error = self._validate_rule(event_data, event_name, rule) if error: errors.append(error) # Event-spezifisches Schema schema = self._schemas.get(event_name) or self._schemas.get("*") if schema: # Event-Klassen-Prüfung if schema.event_class and not isinstance(event_data, schema.event_class): errors.append(EventValidationError( f"Falscher Event-Typ: erwartet {schema.event_class.__name__}", event_name, )) # Regeln prüfen for rule in schema.rules: error = self._validate_rule(event_data, event_name, rule) if error: errors.append(error) # Custom Validator if schema.custom_validator: try: if not schema.custom_validator(event_data): errors.append(EventValidationError( "Benutzerdefinierte Validierung fehlgeschlagen", event_name, )) except Exception as e: errors.append(EventValidationError( f"Validierungsfehler: {e}", event_name, )) return errors async def before( self, context: MiddlewareContext, ) -> MiddlewareResult: """ Validiert Event-Daten vor der Verarbeitung. Args: context: Der Middleware-Kontext. Returns: Ergebnis mit CONTINUE oder ABORT. """ errors = self._validate_event(context.event_name, context.event_data) if errors: # Speichere Fehler im Kontext context.set_property("validation_errors", errors) # Callbacks aufrufen if self._on_validation_error: for error in errors: self._on_validation_error(error) if self._strict: # In striktem Modus: Abbrechen context.errors.extend(errors) return MiddlewareResult( action=MiddlewareAction.ABORT, error=errors[0], ) return MiddlewareResult() class SchemaBuilder: """ Builder für Event-Schemas (Fluent-API). """ def __init__( self, schema: EventSchema, middleware: ValidationMiddleware, ) -> None: """ Initialisiert den Builder. Args: schema: Das Schema. middleware: Die Middleware. """ self._schema = schema self._middleware = middleware def rule( self, field: str, validator: Callable[[Any], bool], message: str = "Validierung fehlgeschlagen", ) -> "SchemaBuilder": """ Fügt eine Regel hinzu. Args: field: Feldname. validator: Validierungsfunktion. message: Fehlermeldung. Returns: Self für Verkettung. """ self._schema.add_rule(field, validator, message) return self def require( self, field: str, message: str | None = None, ) -> "SchemaBuilder": """ Markiert ein Feld als erforderlich. Args: field: Feldname. message: Optionale Fehlermeldung. Returns: Self für Verkettung. """ self._schema.add_rule( field=field, validator=lambda x: True, # Immer gültig wenn vorhanden message=message or f"Pflichtfeld fehlt: {field}", required=True, ) return self def not_none( self, field: str, message: str | None = None, ) -> "SchemaBuilder": """ Verbietet None für ein Feld. Args: field: Feldname. message: Optionale Fehlermeldung. Returns: Self für Verkettung. """ self._schema.add_rule( field=field, validator=lambda x: True, message=message or f"Feld darf nicht None sein: {field}", allow_none=False, ) return self def type_check( self, field: str, expected_type: type, message: str | None = None, ) -> "SchemaBuilder": """ Prüft den Typ eines Feldes. Args: field: Feldname. expected_type: Erwarteter Typ. message: Optionale Fehlermeldung. Returns: Self für Verkettung. """ self._schema.add_rule( field=field, validator=lambda x: isinstance(x, expected_type), message=message or f"Falscher Typ für {field}: erwartet {expected_type.__name__}", ) return self def range( self, field: str, min_val: float | None = None, max_val: float | None = None, message: str | None = None, ) -> "SchemaBuilder": """ Prüft einen Wertebereich. Args: field: Feldname. min_val: Minimalwert. max_val: Maximalwert. message: Optionale Fehlermeldung. Returns: Self für Verkettung. """ def validator(x: Any) -> bool: if min_val is not None and x < min_val: return False if max_val is not None and x > max_val: return False return True range_str = "" if min_val is not None and max_val is not None: range_str = f"[{min_val}, {max_val}]" elif min_val is not None: range_str = f">= {min_val}" elif max_val is not None: range_str = f"<= {max_val}" self._schema.add_rule( field=field, validator=validator, message=message or f"Wert für {field} außerhalb des Bereichs {range_str}", ) return self def length( self, field: str, min_len: int | None = None, max_len: int | None = None, message: str | None = None, ) -> "SchemaBuilder": """ Prüft die Länge eines Feldes. Args: field: Feldname. min_len: Minimale Länge. max_len: Maximale Länge. message: Optionale Fehlermeldung. Returns: Self für Verkettung. """ def validator(x: Any) -> bool: try: length = len(x) if min_len is not None and length < min_len: return False if max_len is not None and length > max_len: return False return True except TypeError: return False self._schema.add_rule( field=field, validator=validator, message=message or f"Ungültige Länge für {field}", ) return self def pattern( self, field: str, regex: str, message: str | None = None, ) -> "SchemaBuilder": """ Prüft ein Regex-Pattern. Args: field: Feldname. regex: Regex-Pattern. message: Optionale Fehlermeldung. Returns: Self für Verkettung. """ import re pattern = re.compile(regex) self._schema.add_rule( field=field, validator=lambda x: bool(pattern.match(str(x))), message=message or f"Wert für {field} entspricht nicht dem Pattern", ) return self def one_of( self, field: str, values: list[Any], message: str | None = None, ) -> "SchemaBuilder": """ Prüft, ob der Wert in einer Liste ist. Args: field: Feldname. values: Erlaubte Werte. message: Optionale Fehlermeldung. Returns: Self für Verkettung. """ self._schema.add_rule( field=field, validator=lambda x: x in values, message=message or f"Wert für {field} nicht erlaubt: {values}", ) return self def custom( self, validator: Callable[["EventData"], bool], ) -> "SchemaBuilder": """ Setzt einen benutzerdefinierten Validator. Args: validator: Validierungsfunktion. Returns: Self für Verkettung. """ self._schema.custom_validator = validator return self def event_class( self, cls: Type["EventData"], ) -> "SchemaBuilder": """ Setzt die erwartete Event-Klasse. Args: cls: Die Event-Klasse. Returns: Self für Verkettung. """ self._schema.event_class = cls return self def build(self) -> EventSchema: """ Gibt das fertige Schema zurück. Returns: Das Schema. """ return self._schema